Libraries.

In [1]:
import os

Beginner

What are context managers ?


Context managers are a clean way to interact with external ressources in python.

Let's look at an example.

ToC

In [2]:
# Accessing the file with a context manager ...
with open('file.txt', 'r') as file:
    # ... The file is used inside the context manager.s
    print('File content : ')
    print(file.read())

# Cheking if the file is properly closed.
print(f'Is the file properly closed : {file.closed}')
File content : 
Line 1
Line 2
Line 3


Is the file properly closed : True

In this simple example, we accessed a file, read its content and checked that the file was properly closed in the end.


This can be done without a context manager, like so :

In [3]:
# Setting up the ressource.
file = open('file.txt', 'r')

# Using the ressource.
print('File content : ')
print(file.read())

# Tearing down the ressource.
file.close()

# Cheking if the file is properly closed.
print(f'Is the file properly closed : {file.closed}')
File content : 
Line 1
Line 2
Line 3


Is the file properly closed : True

The result appear to be the same so this raise the question :


Why use a context manager ?

  • Context managers handle the ressource's set up and tear down. This means that if an error orrur while the ressource is being used, the context manager is still going to tear down the ressource, avoiding memory leaks, and other problems.
  • Using a context manager is a cleaner way of handling ressources and viewed as more pythonic.

When to use a context manager ?

  • Context manager are great when a ressource must be :
    • Set up.
    • Used.
    • Torn down.
  • Common use-cases of context managers are :
    • Working with files.
    • Working with streams.
    • Working with connectors.

Intermediate

Context manager alternative.


There is a possible alternative to using a context manager. A try / finally block can do the trick. Our example then becomes :

ToC

In [4]:
# The ressource is set up and used inside the try clause to catch any error at this point.
try:
    # Setting up the ressource.
    file = open('file.txt', 'r')

    # Using the ressource.
    print('File content : ')
    print(file.read())

finally:
    # Tearing down the ressource.
    file.close()

# Cheking if the file is properly closed.
print(f'Is the file properly closed : {file.closed}')
File content : 
Line 1
Line 2
Line 3


Is the file properly closed : True

The end result is the same, but the context manager version is considered more pythonic, and is a but cleaner.

Intermediate

Creating a context manager.


Creating a context manager can be useful and is not overly complicated.

The following example is a simple file handler. It does not have any practical use because, it basically does what the open function does, but it is a good example.

ToC

In [5]:
class Open_file:
    """A file opener class."""

    def __init__(self, file_name, mode):
        self.file_name = file_name
        self.mode = mode

First let's see what happen if we try to use a simple class in a context manager.

In [6]:
try:
    with Open_file(file_name='file.txt', mode='r'):
        pass
except AttributeError:
    print("Open_file does not have a '__enter__' and '__exit__' method.")
Open_file does not have a '__enter__' and '__exit__' method.

Python rely on the '__enter__' method to set up the ressource and on the '__exit__' method to tear it down. Let's add those to the Open_file class.

In [7]:
class Open_file:
    """A file opener class."""

    def __init__(self, file_name, mode):
        self.file_name = file_name
        self.mode = mode

    def __enter__(self):
        """Set up the file."""
        self.file = open(self.file_name, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, traceback):
        """Tear down the file."""
        self.file.close()

Now let's test try this out.

In [8]:
# Accessing the file with our context manager ...
with Open_file('file.txt', 'r') as file:
    # ... The file is used inside the context manager.s
    print('File content : ')
    print(file.read())

# Cheking if the file is properly closed.
print(f'Is the file properly closed : {file.closed}')
File content : 
Line 1
Line 2
Line 3


Is the file properly closed : True

The new context manager works as intendend. Let's provoke an error to see if the file is still closed properly.

In [9]:
# Accessing the file with our context manager ...
try:
    with Open_file('file.txt', 'r') as file:
        # ... The file is used inside the context manager.s
        print('File content : ')
        print(file.read())
        10 / 0

except ZeroDivisionError:
    print('Forced error.')

# Cheking if the file is properly closed.
print(f'Is the file properly closed : {file.closed}')
File content : 
Line 1
Line 2
Line 3


Forced error.
Is the file properly closed : True

The error occurs inside our context manager, so before reaching the end of the context manager block that is supposed to handle the tearing down of the file, but we can see that the tearing down step still occurs.

Advanced

Creating a context manager (advanced).


There is a slightly more complex but more elegant way of creating a context manager. It is not a lot more complicated than with a class but it requires a few more intermediate notions : the yield statement and decorators.

Let's rewrite our last context manager.

ToC

In [10]:
from contextlib import contextmanager


@contextmanager
def open_file(file_name, mode):
    """Open and manage a file."""
    try:
        file = open(file_name, mode)
        yield file
    finally:
        file.close()

The contextmanager decorator from the contextlib library allow to simply create a context manager using a yield statement.

The yield statement signals that at this point, the function is no longer executed, the file is returned and the rest of the function is only exectued when we reach the end of the context manager's block. Let's see it in practice :

In [11]:
# Accessing the file with our context manager ...
with open_file('file.txt', 'r') as file:
    # ... The file is used inside the context manager.s
    print('File content : ')
    print(file.read())

# Cheking if the file is properly closed.
print(f'Is the file properly closed : {file.closed}')
File content : 
Line 1
Line 2
Line 3


Is the file properly closed : True

Our context manager work properly.

Advanced

A practical example.


The last example is not very useful because the open built-in function already does all that.

A more practical example would be a context manager that allow to work in a specific directory on the machine. This would allow to go to a location, handle so files, and wathever happens to return to the original location.

ToC

In [12]:
@contextmanager
def change_dir(str_path):
    """Change the working directory."""
    try:
        # Storing the current working directory in memory to be able to get back to it later.
        str_cwd = os.getcwd()

        # Changing the working directory than yielding. The yield does not need to return an object in this case.
        os.chdir(str_path)
        yield

    finally:
        # Whatever happens, the current working directory is reverted back to the first one.
        os.chdir(str_cwd)

This context manager allow to execute a specific piece of code inside a directory and go back to the original working directory when we are done.

In [13]:
with change_dir('/Users/tristan/Documents/'):
    with open_file('test.txt', 'w') as file:
        file.write('A sample text.')

Let's see if this worked.

In [14]:
with change_dir('/Users/tristan/Documents/'):
    with open_file('test.txt', 'r') as file:
        str_text = file.read()
        print(str_text)

    os.remove('test.txt')
A sample text.

We were able to access and read the file created before.