Python Context Managers — with Statement Explained
A context manager is a Python object that defines a temporary context for a block of code, ensuring that setup and teardown happen automatically. The with statement is how you use them — it guarantees resources are cleaned up even if an exception occurs.
What You’ll Learn
- The
withstatement and why it’s essential for resource management - Implementing context managers with
__enter__and__exit__ - The
@contextmanagerdecorator for simpler syntax contextlib.ExitStackfor managing multiple contexts- Real-world examples: file handles, DB connections, threading locks
Why Context Managers Matter
Every time you open a file, acquire a lock, or connect to a database, you create a resource that must be released. Forgetting to close a file might not crash your program, but forgetting to release a database connection will exhaust the connection pool. Durga Antivirus Pro uses context managers for virus definition file handles, scan locks, and database sessions. DodaZIP uses them for archive handles and temporary extraction directories.
flowchart LR
A["Generators"] --> B["Context Managers"]
B --> C["Error Handling"]
C --> D["Async"]
D --> E["Type Hints"]
A:::done --> B:::current
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#2563eb,stroke:#2563eb,color:#fff
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
__init__) and Python error handling. Review https://tutorials.dodatech.com/programming-languages/python/py-oop/ and https://tutorials.dodatech.com/programming-languages/python/py-error-handling/ first.The with Statement
# Without context manager — manual cleanup
f = open("file.txt", "w")
try:
f.write("Hello")
finally:
f.close()
# With context manager — automatic cleanup
with open("file.txt", "w") as f:
f.write("Hello")
# File is automatically closed hereThe with statement calls f.__enter__() at the start and f.__exit__() at the end — even if an exception occurs in between.
Creating a Context Manager: Class-Based
A context manager class needs __enter__ and __exit__:
class ManagedFile:
def __init__(self, filename: str, mode: str = "r"):
self.filename = filename
self.mode = mode
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file # Bound to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
# Return False to propagate exceptions (default)
# Return True to suppress exceptions
with ManagedFile("hello.txt", "w") as f:
f.write("Hello, Context Manager!")
with ManagedFile("hello.txt") as f:
print(f.read()) # Hello, Context Manager!The __exit__ method receives three arguments: exception type, value, and traceback. If no exception occurred, all three are None.
The @contextmanager Decorator
For simple cases, use the contextmanager decorator from contextlib:
from contextlib import contextmanager
@contextmanager
def managed_file(filename: str, mode: str = "r"):
file = open(filename, mode)
try:
yield file # This is where the with-block runs
finally:
file.close()
with managed_file("hello.txt") as f:
print(f.read())The function yields exactly once — the yielded value becomes the as variable. Code before yield is __enter__, code after is __exit__.
Real-World Examples
Database Connection
import sqlite3
from contextlib import contextmanager
@contextmanager
def db_connection(db_path: str):
conn = sqlite3.connect(db_path)
try:
yield conn
conn.commit() # Auto-commit on success
except Exception:
conn.rollback() # Rollback on error
raise
finally:
conn.close()
with db_connection("/tmp/test.db") as conn:
conn.execute("CREATE TABLE IF NOT EXISTS users (id INT, name TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'Alice')")
# Auto-committed and closedThreading Locks
import threading
lock = threading.Lock()
# Without context manager
lock.acquire()
try:
# critical section
pass
finally:
lock.release()
# With context manager
with lock:
# critical section
passThe threading.Lock class already implements the context manager protocol.
Timing with Context Manager
import time
from contextlib import contextmanager
@contextmanager
def timer(label: str = "Block"):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label} took {elapsed:.4f}s")
with timer("Database query"):
time.sleep(0.5)
# Database query took 0.5002sExitStack — Managing Multiple Contexts
ExitStack lets you manage an arbitrary number of context managers dynamically:
from contextlib import ExitStack
import glob
def process_many_files(pattern: str):
"""Process multiple files, ensuring all are closed."""
with ExitStack() as stack:
files = [stack.enter_context(open(f)) for f in glob.glob(pattern)]
# All files open; process them
for f in files:
print(f.read()[:50])
# All files closed automaticallyThis is useful when you don’t know how many resources you’ll need at the start.
Suppressing Exceptions
Use contextlib.suppress to ignore specific exceptions:
from contextlib import suppress
import os
with suppress(FileNotFoundError):
os.remove("temp_file.txt") # No error if file doesn't existCommon Mistakes
1. Forgetting finally in @contextmanager Functions
@contextmanager
def bad():
f = open("file.txt")
yield f
# If yield raises, f.close() never runs!Fix: Always wrap yield in try/finally.
2. Returning a Value from __exit__ to Suppress Exceptions
class Silencer:
def __enter__(self):
return self
def __exit__(self, *args):
return True # Suppresses ALL exceptions!
with Silencer():
1 / 0 # No error — bad!Only return True when you intentionally want to suppress specific exceptions.
3. Using with open() Without the as Clause
with open("file.txt"): # File opens and closes, but you can't read it
passFix: Use with open("file.txt") as f:.
4. Not Using Context Managers for Locks
Manual acquire()/release() is error-prone — if an exception occurs between them, the lock is never released.
5. Nesting Too Many with Statements
with open("a") as a:
with open("b") as b:
with open("c") as c:
passFix: Use comma-separated contexts in one with:
with open("a") as a, open("b") as b, open("c") as c:
passPractice Questions
1. What does __exit__ return?
A boolean. True suppresses exceptions. False (default) lets exceptions propagate.
2. When does the cleanup code in @contextmanager run?
When the with block exits — either normally or via exception. The finally clause inside the generator runs.
3. What’s the difference between with A() as a, B() as b: and nested with?
They’re equivalent — the comma-separated form is just more readable.
4. Why does threading.Lock work with with?
Because it implements __enter__ (acquire) and __exit__ (release).
Challenge: Create a temp_directory() context manager that creates a temporary directory, yields its path, then deletes it and all contents on exit.
Solution
import tempfile
import shutil
from contextlib import contextmanager
from pathlib import Path
@contextmanager
def temp_directory():
path = Path(tempfile.mkdtemp())
try:
yield path
finally:
shutil.rmtree(path)Mini Project: Database Transaction Manager
import sqlite3
from contextlib import contextmanager
from typing import Optional
@contextmanager
def transaction(db_path: str):
"""Database transaction with automatic commit/rollback."""
conn = sqlite3.connect(db_path)
try:
yield conn
conn.commit()
print("Transaction committed")
except Exception as e:
conn.rollback()
print(f"Transaction rolled back: {e}")
raise
finally:
conn.close()
# Usage
def transfer_funds(from_id: int, to_id: int, amount: float) -> None:
with transaction("/tmp/bank.db") as conn:
conn.execute(
"UPDATE accounts SET balance = balance - ? WHERE id = ?",
(amount, from_id),
)
conn.execute(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
(amount, to_id),
)
# If any query fails, the transaction rolls back
# Setup test table
with sqlite3.connect("/tmp/bank.db") as conn:
conn.execute("CREATE TABLE IF NOT EXISTS accounts (id INT, balance REAL)")
conn.execute("DELETE FROM accounts")
conn.execute("INSERT INTO accounts VALUES (1, 1000)")
conn.execute("INSERT INTO accounts VALUES (2, 500)")
# Transfer $200 from account 1 to account 2
transfer_funds(1, 2, 200)
print("Transfer complete")Expected output:
Transaction committed
Transfer completeIf an exception occurs (e.g., insufficient funds), the transaction rolls back and neither account changes.
What’s Next
Context managers are a cornerstone of robust Python. Next, learn to write tests for your context managers.
| Topic | Description | Link |
|---|---|---|
| Python Testing | pytest and fixtures | https://tutorials.dodatech.com/programming-languages/python/py-testing/ |
| Python Error Handling | Exceptions and logging | https://tutorials.dodatech.com/programming-languages/python/py-error-handling/ |
| Python Decorators | Extending functions with @ | https://tutorials.dodatech.com/programming-languages/python/py-decorators/ |
Practice tip: Wrap your BankAccount class operations in a database transaction context manager to ensure atomic deposits and withdrawals.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro