Context Managers
Learn how the `with` statement guarantees cleanup even when things go wrong, and how to write your own context managers using classes or a simple decorator.
Imagine you borrow a library book. You must return it when you're done — whether you finished reading it, got interrupted, or even spilled coffee on it. Python's context managers work the same way: they guarantee that something gets cleaned up after you're done with it, no matter what happens.
The classic example is opening a file. If your program crashes mid-way, a context manager makes sure the file is still properly closed. No leaks, no corruption, no mess.
See it in action
— step through the idea, then dive into the details below.Cleanup That Never Gets Skipped
A context manager guarantees that cleanup code runs when you leave a block — even if an error crashes the party. Think of returning a library book: you always have to return it, no matter what happened while you were reading.
#The Problem Without Context Managers
# The risky way — what if an error happens before close()?
f = open("data.txt", "w")
f.write("Hello!")
# Imagine a crash here...
f.close() # This might never run!Leaving files (or database connections, network sockets, locks) open is a real problem. It wastes resources and can corrupt data. The with statement was invented to solve this.
#The with Statement
# The safe way using a context manager
with open("data.txt", "w") as f:
f.write("Hello!")
# File is automatically closed here, even if an error occurredThe Hotel Room Key Card
Think of with like a hotel room. When you check in (__enter__), the lights come on and the AC starts. When you check out (__exit__), everything shuts off automatically — you don't have to remember to flip every switch yourself.
#How It Works: __enter__ and __exit__
Any object can be used with with as long as it has two special methods: - __enter__: runs when the with block starts. Returns the value assigned to as. - __exit__: runs when the with block ends — even if an exception was raised.
Let's look at a simple timer context manager built as a class.
import time
class Timer:
def __enter__(self):
self.start = time.time() # Record start time
return self # This becomes the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.time() - self.start
print(f"Elapsed: {elapsed:.3f} seconds")
return False # Don't suppress any exceptions
with Timer() as t:
total = sum(range(1_000_000))The three parameters in __exit__ (exc_type, exc_val, exc_tb) tell you whether an exception happened inside the block. If everything went fine, all three are None. If you return True from __exit__, Python suppresses the exception. Returning False (or None) lets it propagate normally.
Returning False is Usually Right
In most context managers you should return False from __exit__. Returning True silently swallows errors, which makes bugs hard to find. Only suppress exceptions intentionally and deliberately.
#A Full Example: Custom File Logger
class ManagedFile:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
if exc_type:
print(f"An error occurred: {exc_val}")
return False
with ManagedFile("notes.txt", "w") as f:
f.write("Context managers are great!")#The Easier Way: contextlib.contextmanager
Writing a whole class just for setup and teardown can feel like a lot. Python's contextlib module gives you a much simpler option: the @contextmanager decorator. You write a generator function with exactly one yield. Everything before the yield is setup; everything after is teardown.
from contextlib import contextmanager
@contextmanager
def timer():
import time
start = time.time()
yield # The with block runs right here
elapsed = time.time() - start
print(f"Elapsed: {elapsed:.3f} seconds")
with timer():
total = sum(range(1_000_000))yield the Value You Want as the 'as' Variable
If you want with my_cm() as x: to bind something to x, just yield that value. For example, yield connection makes connection available inside the block.
from contextlib import contextmanager
@contextmanager
def managed_file(path, mode):
f = open(path, mode)
try:
yield f # f is available as the 'as' variable
finally:
f.close() # Always runs, even on error
with managed_file("notes.txt", "w") as f:
f.write("Written safely!") Forgetting try/finally in @contextmanager
If you use @contextmanager without wrapping the yield in try/finally, an exception inside the with block will skip your cleanup code entirely. Always use try: yield ... finally: cleanup() when teardown is important.
#Real-World Uses
Context managers show up everywhere in real Python programs: - Files: open() is the most common built-in context manager. - Database connections: open a connection, do work, close it. - Locks (threading): acquire a lock, do protected work, release it. - Temporary directories: create a temp folder, use it, delete it. - Timers and profilers: measure how long a block takes. - Mocking in tests: patch a function, run the test, unpatch it.
import threading
lock = threading.Lock()
# Without context manager (risky):
lock.acquire()
try:
print("Protected work")
finally:
lock.release()
# With context manager (clean and safe):
with lock:
print("Protected work")#Nesting and Multiple Context Managers
# You can open multiple context managers on one line
with open("input.txt", "r") as src, open("output.txt", "w") as dst:
for line in src:
dst.write(line.upper())What is the primary purpose of a context manager in Python?
Key takeaways
- The `with` statement guarantees cleanup code runs even when exceptions occur.
- Any class with `__enter__` and `__exit__` methods can be used as a context manager.
- The `@contextmanager` decorator lets you write a context manager as a simple generator function with one `yield`.
- Always wrap `yield` in `try/finally` inside `@contextmanager` to ensure teardown always runs.
- Context managers are used everywhere: files, locks, database connections, timers, and test mocking.
What does this code print?
class Greeter:
def __enter__(self):
print("Opening")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Closing")
return False
with Greeter():
print("Inside")This context manager using @contextmanager has a bug — if an exception happens inside the with block, the cleanup code won't run. How should it be fixed?
from contextlib import contextmanager
@contextmanager
def open_resource():
print("Setup")
yield
print("Cleanup") # May be skipped on error!Complete the class so it can be used as a context manager with the with statement. The __enter__ method should return self, and __exit__ should print "Done" and return False.
class Resource: def ____(self): print("Starting") return self def ____(self, exc_type, exc_val, exc_tb): print("Done") return False
Put these lines in the right order to write a working @contextmanager that prints "open" before the block and "closed" after it.
print("open") print("closed")from contextlib import contextmanager
finally:
try:
@contextmanager
yield
def file_like():
Write a context manager called suppress_errors using @contextmanager that silently catches and prints any exception that occurs inside the with block, instead of letting it crash the program. After handling the error, execution should continue normally after the with block.
Example usage: `` with suppress_errors(): print(1 / 0) # ZeroDivisionError — should be caught and printed print("Program continues here") ``
Try it live — edit the code and hit Run to execute real Python: