contextlib

Reviewed & published by Brayan K

The contextlib module gives you tools for working with the with-statement — most notably @contextmanager, which turns a simple generator into a reusable setup-and-cleanup context manager.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

You'll also meet suppress for ignoring specific errors, redirect_stdout for capturing output, and ExitStack for managing a dynamic number of resources at once.

What You'll Learn in This Lesson

1 @contextmanager: Setup, yield, Cleanup

The @contextmanager decorator turns a generator into a context manager. Code before yield is the setup (runs on entering the with); code after yield is the cleanup (runs on exit, even if an error occurs):

from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f"<{name}>")        # setup
    yield                      # body of the with-block runs here
    print(f"</{name}>")       # cleanup

with tag("p"):
    print("Hello!")

# <p>
# Hello!
# </p>
from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f"<{name}>")
    yield
    print(f"</{name}>")

with tag("p"):
    print("Hello!")

# ✅ Expected output:
# <p>
# Hello!
# </p>

2 suppress() and redirect_stdout()

suppress(SomeError) swallows a specific exception so the program keeps going — a clean replacement for try/except: pass. redirect_stdout(buffer) captures everything print() writes into a string buffer instead of the screen:

from contextlib import suppress

# Cleanly ignore a missing file instead of try/except
with suppress(FileNotFoundError):
    open("does_not_exist_12345.txt")

print("No crash - the error was suppressed")
import io
from contextlib import redirect_stdout

buffer = io.StringIO()
with redirect_stdout(buffer):
    print("captured line 1")
    print("captured line 2")

text = buffer.getvalue()
print("Captured", len(text.splitlines()), "lines")
print(repr(text))
# Captured 2 lines
# 'captured line 1\ncaptured line 2\n'
import io
from contextlib import suppress, redirect_stdout

# 1. suppress a specific error
with suppress(FileNotFoundError):
    open("does_not_exist_12345.txt")
print("No crash - the error was suppressed")

# 2. capture printed output into a string
buffer = io.StringIO()
with redirect_stdout(buffer):
    print("captured line 1")
    print("captured line 2")

text = buffer.getvalue()
print("Captured", len(text.splitlines()), "lines")
print(repr(text))

# ✅ Expected output:
# No crash - the error was suppressed
# Captured 2 lines
# 'captured line 1\ncaptured line 2\n'

3 ExitStack and closing()

ExitStack manages a dynamic number of context managers. You call stack.enter_context(cm) for each one, and when the with block ends, all of them are exited in reverse order. Below, three resources are opened from a list and closed automatically:

from contextlib import ExitStack, contextmanager

@contextmanager
def resource(name):
    print(f"open {name}")
    yield name
    print(f"close {name}")

names = ["db", "cache", "log"]
with ExitStack() as stack:
    opened = [stack.enter_context(resource(n)) for n in names]
    print("Using:", opened)
# closes in reverse order: log, cache, db

And closing() adapts any object with a close() method into a context manager:

from contextlib import closing

class Connection:
    def __init__(self):
        print("connected")
    def close(self):
        print("closed")
    def query(self):
        return "rows: 3"

with closing(Connection()) as conn:
    print(conn.query())
# connected / rows: 3 / closed
from contextlib import ExitStack, contextmanager

@contextmanager
def resource(name):
    print(f"open {name}")
    yield name
    print(f"close {name}")

names = ["db", "cache", "log"]
with ExitStack() as stack:
    opened = [stack.enter_context(resource(n)) for n in names]
    print("Using:", opened)

# ✅ Expected output:
# open db
# open cache
# open log
# Using: ['db', 'cache', 'log']
# close log
# close cache
# close db

🎯 Your Turn: Fill in the Blanks

Replace each ___ to finish a context manager that prints a banner before and after the block.

from contextlib import contextmanager

@___                          # hint: the decorator
def banner(title):
    print("=" * 10)
    print(title)
    ___                        # hint: hand control to the with-block
    print("=" * 10)

with banner("REPORT"):
    print("line of data")

# Expected output:
# ==========
# REPORT
# line of data
# ==========

⚠️ Common Errors

❌ Forgetting to yield (or yielding twice)

@contextmanager
def bad():
    print("setup")
    # no yield -> RuntimeError when used in a with
@contextmanager
def good():
    print("setup")
    yield
    print("cleanup")
with suppress(Exception):     # hides real bugs!
    risky()

✅ Suppress only what you expect

with suppress(FileNotFoundError):
    os.remove(path)

❌ Putting cleanup outside the generator

If cleanup must always run, it belongs after the yield (ideally guarded by try/finally), not after the with block — otherwise an exception inside the block skips it.

🎯 Mini Challenge: Capture a Labeled Job

Write a timer_label context manager that prints a start and end line, run it inside redirect_stdout to capture everything, then report how many lines were captured.

import io
from contextlib import contextmanager, redirect_stdout

@contextmanager
def timer_label(label):
    print(f"[{label}] start")
    yield
    print(f"[{label}] end")

buf = io.StringIO()
with redirect_stdout(buf):
    with timer_label("job"):
        print("working...")

captured = buf.getvalue()
print("Lines captured:", len(captured.splitlines()))
print(captured, end="")

# ✅ Expected output:
# Lines captured: 3
# [job] start
# working...
# [job] end

📚 Keep Learning

Frequently Asked Questions

Lesson complete — you write your own context managers!

You can build setup/cleanup logic with @contextmanager and a single yield, ignore precise errors with suppress(), capture output with redirect_stdout, manage runtime-sized resource sets with ExitStack, and adapt close()-able objects with closing().

Practice quiz

What does the @contextmanager decorator turn a generator into?

  • An async function
  • A regular list
  • A reusable context manager you can use with 'with'
  • A decorator factory

Answer: A reusable context manager you can use with 'with'. @contextmanager turns a simple generator into a full context manager usable with 'with'.

In a @contextmanager generator, what does the code BEFORE the yield represent?

  • The setup that runs on entering the with block
  • The cleanup
  • The exception handler
  • Dead code

Answer: The setup that runs on entering the with block. Code before yield is the setup, running when the with block is entered.

What becomes the 'as' variable in a @contextmanager generator?

  • The function name
  • Always None
  • The generator object
  • The value passed to yield

Answer: The value passed to yield. The value yielded becomes the variable bound by 'as' in the with statement.

How many times must a @contextmanager generator yield?

  • Zero times
  • Exactly once
  • Twice
  • At least three times

Answer: Exactly once. It must yield exactly once; yielding zero or multiple times raises a RuntimeError.

What does contextlib.suppress(FileNotFoundError) do?

  • Cleanly ignores that specific exception, like try/except: pass
  • Raises FileNotFoundError
  • Logs the error to a file
  • Retries the operation

Answer: Cleanly ignores that specific exception, like try/except: pass. suppress(SomeError) swallows that specific exception so the program continues — a clean try/except: pass.

Why is 'with suppress(Exception):' considered bad practice?

  • It is slower
  • It only works in Python 3.11+
  • It hides real bugs by swallowing everything broadly
  • It raises a RuntimeError

Answer: It hides real bugs by swallowing everything broadly. Suppressing Exception broadly hides genuine bugs; suppress only the specific error you expect.

What does redirect_stdout(buffer) do?

  • Deletes printed output
  • Captures everything print() writes into the buffer instead of the screen
  • Speeds up printing
  • Redirects input from a file

Answer: Captures everything print() writes into the buffer instead of the screen. redirect_stdout(buffer) captures print() output into a string buffer like io.StringIO.

What problem does contextlib.ExitStack solve?

  • Making code run in parallel
  • Suppressing all output
  • Replacing for-loops
  • Managing a dynamic, unknown-at-write-time number of context managers

Answer: Managing a dynamic, unknown-at-write-time number of context managers. ExitStack lets you enter a runtime-determined number of context managers and clean them all up.

In what order does ExitStack exit the contexts it entered?

  • The same order they were entered
  • Reverse order
  • Random order
  • Alphabetical order

Answer: Reverse order. ExitStack exits the entered contexts in reverse order when the block ends.

What does contextlib.closing(obj) do?

  • Opens a file
  • Suppresses errors
  • Adapts any object with a close() method into a context manager
  • Creates a generator

Answer: Adapts any object with a close() method into a context manager. closing(obj) wraps an object so its close() method is called automatically at block end.

Continue this course

Frequently asked questions

What does @contextmanager actually do?

It turns a simple generator function into a full context manager you can use with 'with'. Everything before the 'yield' runs on entry (setup); the value you yield becomes the 'as' variable; everything after the yield runs on exit (cleanup). It saves you from writing a class with __enter__ and __exit__ methods.

When should I use contextlib.suppress?

Use suppress(SomeError) when you genuinely want to ignore a specific exception and continue. It replaces a try/except pass block with one clean line: 'with suppress(FileNotFoundError): os.remove(path)'. Be specific about which exceptions you suppress — never suppress Exception broadly, or you'll hide real bugs.

What problem does ExitStack solve?

ExitStack lets you enter a variable, unknown number of context managers at runtime and guarantees they all get cleaned up — in reverse order — when the block ends. It's perfect when you need to open a list of files whose count you don't know until the program runs, replacing deeply nested with-statements.

How is closing() different from a plain with?

Some objects have a close() method but don't support the 'with' protocol themselves. contextlib.closing(obj) wraps any such object so that obj.close() is called automatically when the block ends. It's a small adapter that brings non-context-manager resources into the safe 'with' world.