Context Managers
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
Some bugs are not about the main work. They are about the thing you forgot after the work: close the file, restore the setting, print the final log. A context manager is an object that sets something up before a block and cleans it up after the block, so that "before and after" code stays attached to the work it protects.
Revisit the with statement
A resource is something your program borrows and should give back or restore, such as an open file, a temporary setting, or a connection. The with statement is Python syntax that creates a managed block around a resource.
You met with open(...) in file handling. Reuse that notebook picture: opening a file is checking out a notebook, and the with block is the time you are allowed to write in it. When the indented block ends, Python returns the notebook for you.
from pathlib import Path
path = Path("practice_notes.txt")
with open(path, "w", encoding="utf-8") as note:
note.write("Mina: 20 minutes\n")
print("inside block:", note.closed)
print("after block:", note.closed)
with open(path, "r", encoding="utf-8") as note:
print(note.read().strip())
path.unlink() # delete this temporary fileinside block: Falseafter block: TrueMina: 20 minutes
Inside the block, the file is open. After the block, the same file object is closed. The name note still exists, but the resource behind it has been cleaned up.
This matters more when code fails. A with block still gives the context manager a chance to clean up as the block exits. That is the habit: put the risky borrowed thing inside a managed block, not loose in the middle of a long function.
Write a context manager class
The context-manager protocol is the pair of dunder methods Python calls for a with block. __enter__ is the method that runs at the start of the block and returns the value after as. __exit__ is the method that runs when the block ends and receives exception details if something went wrong.
Think of __enter__ as checking out the notebook and __exit__ as returning it, even if your notes got messy.
class PracticeSession:
def __init__(self, player):
self.player = player
self.events = []
def __enter__(self):
print(f"{self.player} starts practice")
return self
def add(self, event):
self.events.append(event)
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
print("practice ended cleanly")
else:
print(f"practice hit {exc_type.__name__}")
for event in self.events:
print(f"- {event}")
return False
with PracticeSession("Noor") as session:
session.add("solved one puzzle")
session.add("earned 30 points")Noor starts practicepractice ended cleanly- solved one puzzle- earned 30 points
__enter__ returns self, so session inside the block is the PracticeSession object. __exit__ receives three values about an exception: its type, its value, and its traceback. When nothing failed, all three are None.
The return False line means "do not hide exceptions." Suppressing an exception means stopping it from continuing after the block. Most context managers should not suppress by default because hidden failures are hard to debug.
Use contextlib and @contextmanager
contextlib is a standard-library module with helpers for building context managers. @contextmanager is a decorator from contextlib that turns a generator function into a context manager.
This works because a generator can pause at yield. Code before yield is setup. The yielded value becomes the value after as. After the managed block finishes, Python resumes the generator at yield. If the block raises an exception, Python throws that exception back at the yield line, so must-run cleanup belongs in finally.
from contextlib import contextmanager
@contextmanager
def score_boost(player, amount):
print(f"prepare boost for {player}")
try:
yield amount
finally:
print(f"clear boost for {player}")
with score_boost("Ravi", 5) as boost:
score = 40 + boost
print(f"score: {score}")prepare boost for Raviscore: 45clear boost for Ravi
The finally block is the cleanup spot. It runs when the managed block exits normally, and it also runs when the managed block exits because of an exception.
Use the class form when the context manager needs several methods or a lot of state. Use @contextmanager when the shape is mostly "do setup, yield one thing, do cleanup."
Try it - manage a tiny file
Run this, then change the player line to write two players. The file is created, used, read, and removed in one run, because the browser playground does not keep files across page reloads.
Restore state after work
Resource management is designing code so borrowed things are released or restored after use. Files are the classic example, but context managers are just as useful for temporary program state.
Here, one small context manager changes a scoreboard setting for a block, then puts the old value back.
from contextlib import contextmanager
settings = {"mood": "quiet"}
@contextmanager
def temporary_mood(mood):
old_mood = settings["mood"]
settings["mood"] = mood
try:
yield
finally:
settings["mood"] = old_mood
print("before:", settings["mood"])
with temporary_mood("focused"):
print("inside:", settings["mood"])
print("after:", settings["mood"])before: quietinside: focusedafter: quiet
That pattern appears all over real Python code:
- Open something, then close it.
- Change a setting, then restore it.
- Start a log section, then finish it.
- Acquire something shared, then release it.
The point is not shorter code. The point is safer code: the cleanup is part of the shape, not a note you hope you remember later.
Checkpoint
Answer all three to mark this lesson complete
Context managers make setup and cleanup visible without making the main action noisy. Next, functional programming gives you another way to shape transformations: small functions, lazy tools, and clear data flow.