Context managers in Python are constructs that allow you to properly manage resources, ensuring that they are acquired and released correctly. They are primarily used with the with statement to handle resources like file handling, database connections, threading locks, etc.
Table of Contents
The Issue
Every resource you open — a file, a database connection, a network socket, a lock — eventually needs to be closed. The naive way to write this looks fine at first glance:
f = open("file.txt")
data = f.read()
f.close()PythonThis works — right up until something goes wrong. If f.read() raises an exception, f.close() never runs. The file handle stays open. Do this enough times in a long-running program (a web server, a data pipeline, a background worker) and you leak file descriptors or database connections until the process falls over.
The instinctive fix is try/finally:
f = open("file.txt")
try:
data = f.read()
finally:
f.close()PythonThis is correct — finally runs whether or not an exception occurred. But it’s verbose, and worse, it’s easy to forget. Nothing forces you to wrap every resource this way. Multiply this pattern across a codebase with files, locks, and connections, and you get:
- Inconsistent cleanup — some code paths remember try/finally, others don’t
- Boilerplate that obscures the actual logic
- Bugs that only show up under load or under failure, exactly when you can least afford them
Python needed a way to bake “always clean up, no matter what” into the language itself, rather than relying on every developer to remember it every time.
Context Managers as the Solution
Context managers in Python are constructs that allow you to properly manage resources, ensuring that they are acquired and released correctly. They are primarily used with the with statement to handle resources like file handling, database connections, threading locks, etc.
A context manager is an object that knows how to set itself up and tear itself down, and the ‘with’ statement is the syntax that guarantees the teardown happens — even if an exception is raised.
with open("file.txt") as f:
data = f.read()PythonNo explicit close(). No try/finally. The file is closed automatically the moment the with block ends, whether it ends normally or via an exception.
Syntax
with expression as variable:
# Code blockPythonCommon Use Cases
- File Handling: with open()
- Database Connections: with sqlite3.connect()
- Thread Locks: with threading.Lock()
- Temporary Files: with tempfile.TemporaryFile()
- Managing Network Connections: with socket.create_connection()
Custom Context Managers
This protocol is two dunder methods:
class MyContext:
def __enter__(self):
print("Entering the context")
return self # The return value is assigned to 'as' variable in 'with'
def __exit__(self, exc_type, exc_value, traceback):
print("Exiting the context")
return False # If exception occurs, it will still call this method
# Usage
with MyContext() as t:
print("Inside the context block")
'''
Entering the context
Inside the context block
Exiting the context
''' Python__enter__()
It is the first method that gets called when you enter a with block using a context manager.
__exit__()
It is always called — whether:
- The with block completes normally, or
- an exception is raised inside the with block.
__enter__ return
Whatever __enter__ returns becomes t. That’s the entire job of the return value — bind a name for use inside the block. Python doesn’t inspect it, check it for True/False, or use it to decide anything.
__exit__ return
- False (or None, which is the default if you don’t return anything) → “I did not handle this exception, let it propagate.” The exception continues as normal, exactly as if the with block weren’t there.
- True → “I’ve handled it, swallow it.” The exception is suppressed — code after the with block continues running as if nothing happened.
The Decorator Shortcut
Writing a full class with __enter__ and __exit__ is fine, but it’s a lot of ceremony for something conceptually simple: run some setup, run the block, run some teardown. The contextlib module gives you a decorator that turns an ordinary generator function into a context manager:
from contextlib import contextmanager
@contextmanager
def my_context():
print("Entering the context")
yield # This is where the block inside 'with' runs
print("Exiting the context")
# Usage
with my_context():
print("Inside the context block")PythonExample
Temporary directory change
class based
import os
class ChangeDir:
def __init__(self, path):
self.path = path
def __enter__(self):
self.old_path = os.getcwd()
os.chdir(self.path)
return self.path
def __exit__(self, exc_type, exc_val, exc_tb):
os.chdir(self.old_path)
return FalsePythonUsages
with ChangeDir("/tmp") as p:
print(os.getcwd()) # /tmp
print(p) # /tmp (this is __enter__'s return value)
print(os.getcwd()) # back to wherever you startedPythonDecorator-based
from contextlib import contextmanager
import os
@contextmanager
def change_dir(path):
old_path = os.getcwd()
os.chdir(path)
try:
yield path
finally:
os.chdir(old_path)PythonUsages
with change_dir("/tmp"):
print(os.getcwd()) # /tmp
print(os.getcwd()) # back to original, guaranteedPythonDatabase-style transaction with commit/rollback
class based
class Transaction:
def __init__(self, conn):
self.conn = conn
def __enter__(self):
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.conn.commit()
else:
self.conn.rollback()
return False # don't swallow the exception, just clean upPythonUsages
with Transaction(db_conn) as conn:
conn.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
conn.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")PythonDecorator-based
@contextmanager
def transaction(conn):
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise # re-raise so the caller still sees the errorPythonUsages
with transaction(db_conn) as conn:
conn.execute("UPDATE ...")
# if this raises, rollback() runs; if it succeeds, commit() runsPythonConclusion
Context managers exist to solve one specific problem: cleanup that must happen no matter what. Whether it’s closing a file, releasing a lock, restoring a directory, or committing/rolling back a database transaction, the pattern is always the same — set something up, run some code, guarantee the teardown even if that code blows up.