Context Managers
Context Managers
A context manager is an object you use with with. It sets something up, yields a value, and tears it down even if the block raises. Files, locks, and “this table is occupied” are the boring uses. The boring default is with for every resource that has a close, and contextlib.contextmanager for a tiny custom one. A class with __enter__ / __exit__ is fine when the state has a name.
Mental model
with manager as name:
...Python calls manager.__enter__() and binds the return value to name. When the block leaves — return, exception, or the last line — it calls manager.__exit__(exc_type, exc, tb).
If __exit__ returns a true value, the exception is swallowed. Return False (or None) to let it propagate. Swallowing is a defect unless you are writing a helper whose job is to ignore a specific error.
@contextmanager turns a generator into the same protocol: code before yield is enter, code in finally after yield is exit.
threading.Lock is a context manager. with lock: acquires and releases. Do not call acquire()/release() by hand unless you are in a situation with cannot express.
Worked examples
Case 1: a file in a temporary directory
Save as shift_file.py. TemporaryDirectory and path.open() are both context managers. Nothing is left on disk when main returns.
# shift_file.py
from pathlib import Path
import tempfile
def main():
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "shift.txt"
path.write_text("open 4\n")
with path.open() as f:
print(f.read().strip())
if __name__ == "__main__":
main()Run:
uv run python shift_file.pyOutput:
open 4
path.write_text closes the file itself. The inner with path.open() is the read side. Nested with is normal.
Case 2: __enter__ and __exit__
Save as occupied.py. Seating a table is a pair of actions. The class holds the table number.
# occupied.py
class Occupied:
def __init__(self, table):
self.table = table
def __enter__(self):
print(f"seat {self.table}")
return self.table
def __exit__(self, exc_type, exc, tb):
print(f"clear {self.table}")
return False
def main():
with Occupied(12) as table:
print(f"serving {table}")
if __name__ == "__main__":
main()Run:
uv run python occupied.pyOutput:
seat 12
serving 12
clear 12
__exit__ still runs if the body raises. Save as occupied_error.py to see the order.
# occupied_error.py
class Occupied:
def __init__(self, table):
self.table = table
def __enter__(self):
print(f"seat {self.table}")
return self.table
def __exit__(self, exc_type, exc, tb):
print(f"clear {self.table}")
return False
def main():
try:
with Occupied(12):
raise ValueError("kitchen closed")
except ValueError as e:
print(e)
if __name__ == "__main__":
main()Run:
uv run python occupied_error.pyOutput:
seat 12
clear 12
kitchen closed
Clear, then the exception. That is the contract.
Case 3: @contextmanager
Save as occupied_cm.py. Same seating, less class. Use this when there is no extra state beyond the arguments.
# occupied_cm.py
from contextlib import contextmanager
@contextmanager
def occupied(table):
print(f"seat {table}")
try:
yield table
finally:
print(f"clear {table}")
def main():
with occupied(12) as table:
print(f"serving {table}")
if __name__ == "__main__":
main()Run:
uv run python occupied_cm.pyOutput:
seat 12
serving 12
clear 12
The try / finally around yield is required so clear runs on error. Forgetting it is the generator version of swallowing cleanup.
Case 4: a lock
Save as desk_lock.py. The counter is shared. The lock makes stamp safe if two threads called it. One thread still uses with lock so the acquire/release pairing cannot drift.
# desk_lock.py
import threading
lock = threading.Lock()
next_id = 0
def stamp():
global next_id
with lock:
next_id += 1
return f"ticket {next_id}"
def main():
print(stamp())
print(stamp())
if __name__ == "__main__":
main()Run:
uv run python desk_lock.pyOutput:
ticket 1
ticket 2
global is ugly. A small class with self.next_id and self.lock is the next step when this grows. The with lock stays.
The trap
__exit__ returning True eats the error. The with looks successful.
# swallow_exit.py
class Occupied:
def __init__(self, table):
self.table = table
def __enter__(self):
print(f"seat {self.table}")
return self.table
def __exit__(self, exc_type, exc, tb):
print(f"clear {self.table}")
return True
def main():
with Occupied(12):
raise ValueError("kitchen closed")
print("kept going")
if __name__ == "__main__":
main()Run:
uv run python swallow_exit.pyOutput:
seat 12
clear 12
kept going
The kitchen closed and main continued. Return False. If you meant to handle ValueError, catch it inside the block or outside the with, not by a boolean from __exit__.
The boring rule
withfor files, locks, temp directories, and any pair of setup/teardown.- Class with
__enter__/__exit__when the manager has state.@contextmanagerwhen it is a short generator. __exit__returnsFalse. Cleanup goes there; handling goes inexcept.- Put
yieldinsidetry/finallyin a@contextmanagerfunction. - Nest
withor usewith a, b:for two managers.ExitStackis the next chapter. - Do not open a file in
__init__and hope the caller closes it.
Try this
- In
shift_file.py, write two lines and printf.readline().strip()twice inside thewith. - Change
occupied.pyso__enter__returnsself. Printctx.tableinside the block. - In
occupied_cm.py, raiseValueError("kitchen closed")inside thewithand catch it inmain. Confirmclearstill prints. - Return
Falsefromswallow_exit.py’s__exit__and catch theValueErrorinmain.