Python Context Managers: The with Statement and Building Your Own
Imagine walking through an automatic door at a grocery store. You don't have to push it open or remember to close it behind you. The door opens when you approach and closes when you walk through. It just handles itself.
Python context managers work the same way. They automatically set things up when you enter a block of code and clean things up when you leave. No matter what happens inside — even if an error occurs — the cleanup always runs.
In this tutorial, you'll learn how the with statement works, what happens behind the scenes with __enter__ and __exit__, and how to build your own context managers from scratch.
What Is the with Statement?
The with statement is Python's way of saying: "set something up, let me work with it, and then clean it up automatically." You've probably already seen it when working with files.
f = open('data.txt', 'w')
try:
f.write('Hello!')
finally:
f.close() # You must remember this!with open('data.txt', 'w') as f:
f.write('Hello!')
# File is automatically closed hereBoth versions do the same thing. But the with version is shorter, cleaner, and impossible to mess up. You can't forget to close the file because Python does it for you.
Let's see a simpler example we can actually run. Python's built-in open() won't work in our browser environment, so we'll build our own context managers to understand the concept.
Notice the order of the output: "Hello" runs first when we enter, then our work happens, then "Goodbye" runs automatically when the block ends.
How Do __enter__ and __exit__ Work?
Every context manager is just an object with two special methods: __enter__ and __exit__. When Python hits a with statement, it calls these methods in order.
Here's what happens step by step:
__enter__() on the object__enter__() gets assigned to the variable after as__exit__() — even if an error occurred in step 3What Happens When an Error Occurs Inside with?
The real superpower of context managers is that __exit__ runs even when things go wrong. This guarantees your cleanup code always executes.
The cleanup message prints before the except catches the error. This is what makes context managers so reliable — they always get a chance to run their teardown logic.
You can also choose to suppress an error by returning True from __exit__. This tells Python: "I handled this error, don't raise it."
How Do You Build a Class-Based Context Manager?
Let's build something practical: a timer that measures how long a block of code takes to run. This is a common real-world use case.
Here's another useful pattern — a context manager that temporarily changes a value and restores it when done.
How Do You Build a Context Manager with contextlib?
Writing a full class with __enter__ and __exit__ is fine, but Python gives you a shortcut. The contextlib module lets you write a context manager as a simple generator function.
The function splits into three parts:
__enter__)as variable__exit__)What Are Some Real-World Context Manager Patterns?
Context managers show up everywhere in professional Python code. Here are patterns you'll encounter in real projects.
Pattern 1: Nested context managers. You can nest multiple with statements or use a comma to combine them.
Pattern 2: Transaction-like behavior. Set up a state, do work, then commit or roll back.
Practice Exercises
What will this code print? Think carefully about the order of __enter__, the block body, and __exit__.
class Door:
def __enter__(self):
print('Door opens')
return 'welcome'
def __exit__(self, exc_type, exc_val, exc_tb):
print('Door closes')
return False
with Door() as msg:
print(msg)Create a class called LogBlock that works as a context manager. It takes a label string in __init__.
__enter__ should print START: <label> and return self.__exit__ should print END: <label> and return False.Then use it: with LogBlock('test') as lb: and inside print Running....
This context manager is supposed to always print the cleanup message, even when an error occurs inside the block. But the cleanup never runs when there's an error. Fix the @contextmanager function so cleanup always happens.
The expected output should be:
Setup done
Working...
Cleanup doneCreate a context manager called indent_block using @contextmanager from contextlib. It should take a level parameter (an integer).
'Entering level <level>'' ' * level (two spaces repeated level times)'Exiting level <level>'try/finally for safety.Then demonstrate with:
with indent_block(1) as pad:
print(f'{pad}Hello')
with indent_block(2) as pad2:
print(f'{pad2}World')Refactor this code to use a context manager class called ConnectionSimulator instead of try/finally. The output must remain exactly the same.
Original code:
connected = False
try:
connected = True
print('Connected: True')
print('Sending data...')
finally:
connected = False
print('Connected: False')Your class should set self.connected in __enter__ and reset it in __exit__. Print the same messages.
Create a class called BatchProcessor that collects items and processes them when the block ends.
__init__: set self.items to an empty list__enter__: print 'Batch started' and return selfadd(self, item) that appends the item to self.items__exit__: print 'Processing N items...' where N is the count, then print each item as ' - <item>', then print 'Batch complete'. Return False.Use it to add 'email', 'report', and 'backup'.