Advanced PythonAdvanced10 min56 / 63

Context Managers

Learn how the `with` statement guarantees cleanup even when things go wrong, and how to write your own context managers using classes or a simple decorator.

Imagine you borrow a library book. You must return it when you're done — whether you finished reading it, got interrupted, or even spilled coffee on it. Python's context managers work the same way: they guarantee that something gets cleaned up after you're done with it, no matter what happens.

The classic example is opening a file. If your program crashes mid-way, a context manager makes sure the file is still properly closed. No leaks, no corruption, no mess.

See it in action

Visual walkthrough1 / 6
1

Cleanup That Never Gets Skipped

A context manager guarantees that cleanup code runs when you leave a block — even if an error crashes the party. Think of returning a library book: you always have to return it, no matter what happened while you were reading.

The `with` statement is Python's built-in way to use context managers.

#The Problem Without Context Managers

If anything goes wrong between open() and close(), the file stays open.
# The risky way — what if an error happens before close()?
f = open("data.txt", "w")
f.write("Hello!")
# Imagine a crash here...
f.close()  # This might never run!

Leaving files (or database connections, network sockets, locks) open is a real problem. It wastes resources and can corrupt data. The with statement was invented to solve this.

#The with Statement

The file closes the moment the with block exits — success or error.
# The safe way using a context manager
with open("data.txt", "w") as f:
    f.write("Hello!")
# File is automatically closed here, even if an error occurred
Think of it like

The Hotel Room Key Card

Think of with like a hotel room. When you check in (__enter__), the lights come on and the AC starts. When you check out (__exit__), everything shuts off automatically — you don't have to remember to flip every switch yourself.

#How It Works: __enter__ and __exit__

Any object can be used with with as long as it has two special methods: - __enter__: runs when the with block starts. Returns the value assigned to as. - __exit__: runs when the with block ends — even if an exception was raised.

Let's look at a simple timer context manager built as a class.

A reusable Timer that reports how long a block of code took.
import time

class Timer:
    def __enter__(self):
        self.start = time.time()  # Record start time
        return self              # This becomes the 'as' variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        elapsed = time.time() - self.start
        print(f"Elapsed: {elapsed:.3f} seconds")
        return False  # Don't suppress any exceptions

with Timer() as t:
    total = sum(range(1_000_000))

The three parameters in __exit__ (exc_type, exc_val, exc_tb) tell you whether an exception happened inside the block. If everything went fine, all three are None. If you return True from __exit__, Python suppresses the exception. Returning False (or None) lets it propagate normally.

Note

Returning False is Usually Right

In most context managers you should return False from __exit__. Returning True silently swallows errors, which makes bugs hard to find. Only suppress exceptions intentionally and deliberately.

#A Full Example: Custom File Logger

A class-based context manager that closes the file and logs any errors.
class ManagedFile:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()
        if exc_type:
            print(f"An error occurred: {exc_val}")
        return False

with ManagedFile("notes.txt", "w") as f:
    f.write("Context managers are great!")

#The Easier Way: contextlib.contextmanager

Writing a whole class just for setup and teardown can feel like a lot. Python's contextlib module gives you a much simpler option: the @contextmanager decorator. You write a generator function with exactly one yield. Everything before the yield is setup; everything after is teardown.

The @contextmanager decorator turns a generator into a context manager with far less code.
from contextlib import contextmanager

@contextmanager
def timer():
    import time
    start = time.time()
    yield              # The with block runs right here
    elapsed = time.time() - start
    print(f"Elapsed: {elapsed:.3f} seconds")

with timer():
    total = sum(range(1_000_000))
Tip

yield the Value You Want as the 'as' Variable

If you want with my_cm() as x: to bind something to x, just yield that value. For example, yield connection makes connection available inside the block.

Use try/finally inside a @contextmanager to guarantee cleanup even when exceptions occur.
from contextlib import contextmanager

@contextmanager
def managed_file(path, mode):
    f = open(path, mode)
    try:
        yield f       # f is available as the 'as' variable
    finally:
        f.close()     # Always runs, even on error

with managed_file("notes.txt", "w") as f:
    f.write("Written safely!")  
Common mistake

Forgetting try/finally in @contextmanager

If you use @contextmanager without wrapping the yield in try/finally, an exception inside the with block will skip your cleanup code entirely. Always use try: yield ... finally: cleanup() when teardown is important.

#Real-World Uses

Context managers show up everywhere in real Python programs: - Files: open() is the most common built-in context manager. - Database connections: open a connection, do work, close it. - Locks (threading): acquire a lock, do protected work, release it. - Temporary directories: create a temp folder, use it, delete it. - Timers and profilers: measure how long a block takes. - Mocking in tests: patch a function, run the test, unpatch it.

Locks are context managers — the with statement always releases the lock.
import threading

lock = threading.Lock()

# Without context manager (risky):
lock.acquire()
try:
    print("Protected work")
finally:
    lock.release()

# With context manager (clean and safe):
with lock:
    print("Protected work")

#Nesting and Multiple Context Managers

Comma-separated context managers in a single with statement — both close cleanly.
# You can open multiple context managers on one line
with open("input.txt", "r") as src, open("output.txt", "w") as dst:
    for line in src:
        dst.write(line.upper())
Quick check

What is the primary purpose of a context manager in Python?

Key takeaways

  • The `with` statement guarantees cleanup code runs even when exceptions occur.
  • Any class with `__enter__` and `__exit__` methods can be used as a context manager.
  • The `@contextmanager` decorator lets you write a context manager as a simple generator function with one `yield`.
  • Always wrap `yield` in `try/finally` inside `@contextmanager` to ensure teardown always runs.
  • Context managers are used everywhere: files, locks, database connections, timers, and test mocking.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

What does this code print?

predict-output
class Greeter:
    def __enter__(self):
        print("Opening")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Closing")
        return False

with Greeter():
    print("Inside")
Fix the bug#2

This context manager using @contextmanager has a bug — if an exception happens inside the with block, the cleanup code won't run. How should it be fixed?

fix-bug
from contextlib import contextmanager

@contextmanager
def open_resource():
    print("Setup")
    yield
    print("Cleanup")  # May be skipped on error!
Fill in the blank#3

Complete the class so it can be used as a context manager with the with statement. The __enter__ method should return self, and __exit__ should print "Done" and return False.

class Resource:
    def ____(self):
        print("Starting")
        return self

    def ____(self, exc_type, exc_val, exc_tb):
        print("Done")
        return False
Reorder the lines#4

Put these lines in the right order to write a working @contextmanager that prints "open" before the block and "closed" after it.

1
    print("open")
2
        print("closed")
3
from contextlib import contextmanager
4
    finally:
5
    try:
6
@contextmanager
7
        yield
8
def file_like():
Your turn
Practice exercise

Write a context manager called suppress_errors using @contextmanager that silently catches and prints any exception that occurs inside the with block, instead of letting it crash the program. After handling the error, execution should continue normally after the with block.

Example usage: `` with suppress_errors(): print(1 / 0) # ZeroDivisionError — should be caught and printed print("Program continues here") ``

Try it live — edit the code and hit Run to execute real Python:

solution.py · editable