Skip to content
Python Context Managers — with Statement Explained

Python Context Managers — with Statement Explained

DodaTech Updated Jun 15, 2026 6 min read

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 with statement and why it’s essential for resource management
  • Implementing context managers with __enter__ and __exit__
  • The @contextmanager decorator for simpler syntax
  • contextlib.ExitStack for 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
  

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 here

The 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 closed

Threading 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
    pass

The 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.5002s

ExitStack — 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 automatically

This 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 exist

Common 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
    pass

Fix: 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:
            pass

Fix: Use comma-separated contexts in one with:

with open("a") as a, open("b") as b, open("c") as c:
    pass

Practice 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 complete

If 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.

TopicDescriptionLink
Python Testingpytest and fixtureshttps://tutorials.dodatech.com/programming-languages/python/py-testing/
Python Error HandlingExceptions and logginghttps://tutorials.dodatech.com/programming-languages/python/py-error-handling/
Python DecoratorsExtending 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