2. Context Managers#
2.1. Introduction#
As you are (hopefully) already accustomed to (at least basic) Python, you’ve almost certainly encountered those fancy and extremely useful structures with the with keyword, called context managers. Where does this name come from? Well, context managers define a runtime environment (context) for a given block of code.
Block of code?
I am sure you know it, but a block of code is a sequence of Python statements within the same indentation, e.g., this block of code for the for loop contains just a single statement: print(i):
for i in range(10):
print(i)
Why do we need such an environment? Well, when you need to configure anything before executing some piece of code (a code block) and/or tear down and clean up afterwards. That’s the core idea behind context managers: set up before, clean up after, guaranteed.
2.2. Running in context#
To run code within the context managed by a context manager, we use the with statement. If the context manager returns any object on setup, you can bind it using the as keyword:
with open("some_dummy_file.txt", "w") as f:
f.write("1st line\n")
f.write("2nd line")
Context managers can be created in two ways: either as a class-based definition or a generator-based definition. Both are presented below.
2.3. Class-based definition#
Welcome OOP
To define a context manager using classes, you need to know what a class is and how we can define it. If you don’t have such knowledge, learn about it and return later.
A context manager can be created as a class which implements two special (dunder[1]) methods:
__enter____exit__
It is quite clear that the first (__enter__) is responsible for setting up the execution context (environment), whereas the latter is responsible for cleaning up.
2.3.1. __enter__#
The setup method is argumentless, namely it takes just an implicit parameter (self), but you can return anything. Remember that the object returned by the __enter__ method can be bound to the target variable using the as keyword.
1class MyContextManager:
2
3 def __enter__(self):
4 # some logic goes here
5 return self # We return the object itself to be able to bind it using `as`
Did you know?
If the __enter__ method does not raise an error, the __exit__ method is guaranteed to be invoked [Python Software Foundation, 2025], regardless of whether the block inside the context manager raises an error or not.
2.3.2. __exit__#
Unlike __enter__, the __exit__ method imposes three parameters:
exception type (type hint:
type[Exception] | None),exception value (type hint:
Exception | None),traceback (type hint:
types.TracebackType | None)
If the block of code inside the context manager does not raise an error, all those arguments will be None. Otherwise, they will be set accordingly. The question is, what to do next with such an error? You have two options:
suppress it (silence it, do not propagate),
reraise it
To suppress the exception (or manage it on your own inside the __exit__ method), you need to return the value True explicitly or any expression evaluated to True [Python Software Foundation, 2025]. Otherwise, the exception will be propagated. Following our above example, let us add a dummy __exit__ implementation:
7 def __exit__(self, exc_type: type[Exception] | None, exc_val: Exception | None, traceback):
8 # some logic goes here
9 return True # If we want to suppress the error, we return True
As an example, we will try to reimplement a simplified version of the chdir context manager from contextlib standard library to change the current working directory.
1import os
2
3class ChDir:
4 saved_path: str
5 new_path: str
6
7 def __init__(self, new_path):
8 self.new_path = new_path # We save the new directory we move into inside the context manager
9
10 def __enter__(self):
11 self.saved_path = os.getcwd() # We store this to restore it later
12 os.chdir(self.new_path) # We actually change directory here
13 print(f"📂 Changed directory to: {self.new_path}")
14 return self # Not required, but we can use it with the `as` keyword
15
16 def __exit__(self, exc_type, exc_value, traceback):
17 os.chdir(self.saved_path) # We restore the original working directory
18 print(f"↩️ Returned to: {self.saved_path}")
Asynchronous generator-based context managers
To create asynchronous context manager (see section below Asynchronous Context Managers) [Python Software Foundation, 2025], you need to use __aenter__ and __aexit__ asynchronous dunder methods. Their signature is the same as their synchronous counterparts.
2.4. Generator-based definition#
Do you know generators?
Before you dive into this section, ensure you are familiar with Python generators. Read 📰 about them in the chapter Generators.
Do you know decorators?
You should be aware of decorators before reading this section. You will find them in the chapter Decorators.
Writing context managers as classes gives us the most flexibility; however, there is another, quite convenient way to create a simple context manager—by using a generator function [Python Software Foundation, 2025]. The pivotal element is the yield statement and the @contextmanager decorator from the contextlib [Python Software Foundation, 2025] module, which is used to decorate the generator function. It has the following structure:
1from contextlib import contextmanager
2
3@contextmanager # We need to use this decorator to make a func a context manager
4def my_context_manager():
5 # here is the logic to run on setup
6 try:
7 yield # We can yield something to be able to bind it using `as`
8 except Exception as e:
9 # logic to handle (optionally) the exception
10 finally:
11 # logic to clean up
So, we have:
Line |
Code |
Explanation |
|---|---|---|
1 |
from contextlib import contextmanager
|
Imports the |
3-4 |
@contextmanager
def my_context_manager():
|
We decorate the function to make it a context manager |
5 |
# here is the logic to run on setup
|
Here (before the |
6-7 |
try:
yield
|
This is where the magic happens. It creates a generator function, so it saves the state and sends a value (here we send nothing, but anything you add after |
8-9 |
except Exception as e:
# logic to handle (optionally) exception
|
Here we catch any exception which occurred inside the context manager. You can catch a more specific error or keep it as general as |
10-11 |
finally:
# logic to clean up
|
As in most programming languages, the |
Asynchronous generator-based context managers
If you intend to create an asynchronous context manager (see section below Asynchronous Context Managers), remember to define the function (decorated with @asynccontextmanager) as async.
Context manager as decorator? Why not?
The contextlib library provides a variety of other useful tools. For example, you can create a context manager as a decorator. To do that, you just follow the class-based definition and add a parent class ContextDecorator, so you can use it as:
1import os
2from contextlib import ContextDecorator
3
4class ChDir(ContextDecorator): # Note, we've added a parent class
5 ...
6
7@ChDir("/tmp")
8def some_func():
9 # some function's logic
10 print(f"I am in {os.getcwd()}")
11
12some_func()
2.5. Usage#
Regardless of the way you defined your context manager, you can use it as below:
print("Before context:", os.getcwd())
with ChDir("/tmp"): # or any folder you have
print("Inside context:", os.getcwd())
# do stuff here safely
print("After context:", os.getcwd())
Before context: /home
📂 Changed directory to: /tmp
Inside context: /tmp
↩️ Returned to: /home
After context: /home
2.6. Asynchronous context managers#
Do you know coroutines?
To understand this section well, read 📰 the Coroutines chapter first.
When possible, such as when performing input/output operations like handling files, opening database connections, or managing HTTP connections, we can create an asynchronous version of a context manager. The dunder methods for creating asynchronous context managers can be coroutines (see the Coroutines chapter). These special methods’ names change slightly: they must be defined using async def, and their names are __aenter__ and __aexit__ for setting up and cleaning up the asynchronous context manager, respectively. As an example, let’s create a simple asynchronous context manager to lock a file in order to prevent it from being overwritten by concurrent tasks (to avoid data inconsistency).
1import asyncio
2
3class AsyncFileLock:
4 filename: str
5 lock: asyncio.Lock
6
7 def __init__(self, filename):
8 self.filename = filename
9 self.lock = asyncio.Lock()
10
11 async def __aenter__(self): # Note: we are using `async` and the name changed
12 print(f"Acquiring lock for {self.filename}...")
13 await self.lock.acquire()
14 print(f"Lock acquired for {self.filename}")
15 return self
16
17 async def __aexit__(self, exc_type, exc_val, exc_tb): # Note: the name is `__aexit__`
18 self.lock.release()
19 print(f"Lock released for {self.filename}")
20 return False
We can then use it as follows:
async def some_long_file_handling_func():
print("Processing file...")
await asyncio.sleep(10)
print("File processing done")
async with Lock("/tmp/sample.txt"):
await some_long_file_handling_func()
Acquiring lock for /tmp/sample.txt...
Lock acquired for /tmp/sample.txt
Processing file...
File processing done
Lock released for /tmp/sample.txt