Mathew K Analytics

Lesson 17 · Python standard library deep dive

Python contextlib Explained: Custom Context Managers | Standard Library #17

Video seventeen of the twenty-five-part series: contextlib, for building and combining context managers. The @contextmanager decorator, closing, suppress,…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Python Standard Library Deep-Dive, Video 17: contextlib#

  • Video seventeen of the twenty-five-part series: contextlib, for building and combining context managers.
  • The @contextmanager decorator, closing, suppress, redirect_stdout, ExitStack, and nullcontext.
  • Let's get into it.

Part 1: Recap of the with Protocol#

class Timer:
    def __enter__(self):
        print('starting')
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('stopping')
        return False
with Timer():
    print('doing work')
starting
doing work
stopping

Part 2: @contextmanager - Generator-Based Context Managers#

from contextlib import contextmanager
@contextmanager
def timer():
    print('starting')
    yield 'timer-object'
    print('stopping')
with timer() as t:
    print(f'doing work with {t}')
starting
doing work with timer-object
stopping

Part 3: Exception Handling Inside @contextmanager#

@contextmanager
def resource_guard(name):
    print(f'acquiring {name}')
    try:
        yield name
    finally:
        print(f'releasing {name}')
try:
    with resource_guard('db-connection'):
        print('using resource')
        raise ValueError('something broke')
except ValueError as e:
    print(f'caught: {e}')
acquiring db-connection
using resource
releasing db-connection
caught: something broke

Part 4: contextlib.closing()#

from contextlib import closing
class Connection:
    def __init__(self, name):
        self.name = name
        self.open = True
    def close(self):
        self.open = False
        print(f'{self.name} closed')
with closing(Connection('conn-a')) as conn:
    print(conn.open)
print(conn.open)
True
conn-a closed
False

Part 5: contextlib.suppress()#

from contextlib import suppress
import os
with suppress(FileNotFoundError):
    os.remove('file_that_does_not_exist.txt')
print('continued past the missing file')
continued past the missing file
try:
    with suppress(FileNotFoundError):
        result = 10 / 0
except ZeroDivisionError:
    print('ZeroDivisionError was NOT suppressed, exactly as expected')
ZeroDivisionError was NOT suppressed, exactly as expected

Part 6: redirect_stdout and redirect_stderr#

import io
from contextlib import redirect_stdout
buffer = io.StringIO()
with redirect_stdout(buffer):
    print('this is captured, not printed')
    print('so is this')
captured = buffer.getvalue()
print(f'captured text: {captured!r}')
captured text: 'this is captured, not printed\nso is this\n'

Part 7: ExitStack - a Dynamic Number of Context Managers#

from contextlib import ExitStack
names = ['a', 'b', 'c']
with ExitStack() as stack:
    resources = [stack.enter_context(resource_guard(name)) for name in names]
    print(resources)
acquiring a
acquiring b
acquiring c
['a', 'b', 'c']
releasing c
releasing b
releasing a
with ExitStack() as stack:
    stack.callback(print, 'cleanup one')
    stack.callback(print, 'cleanup two')
    print('doing work inside the stack')
doing work inside the stack
cleanup two
cleanup one

Part 8: nullcontext#

from contextlib import nullcontext
def process(data, lock=None):
    ctx = lock if lock is not None else nullcontext()
    with ctx:
        return sum(data)
print(process([1, 2, 3]))
import threading
real_lock = threading.Lock()
print(process([4, 5, 6], lock=real_lock))
6
15

Part 9: Using a Context Manager as a Decorator#

@contextmanager
def logged(label):
    print(f'[{label}] entering')
    yield
    print(f'[{label}] exiting')
@logged('demo-call')
def do_something():
    print('inside do_something')
do_something()
[demo-call] entering
inside do_something
[demo-call] exiting

Part 10: A Real Pattern - Temporarily Changing State#

import os
@contextmanager
def temporary_directory_change(path):
    original = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(original)
print(os.getcwd())
with temporary_directory_change('/tmp'):
    print(os.getcwd())
print(os.getcwd())
c:\PythonTraining\Full_videos\Notebooks
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[12], line 11
      9         os.chdir(original)
     10 print(os.getcwd())
---> 11 with temporary_directory_change('/tmp'):
     12     print(os.getcwd())
     13 print(os.getcwd())

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\contextlib.py:137, in _GeneratorContextManager.__enter__(self)
    135 del self.args, self.kwds, self.func
    136 try:
--> 137     return next(self.gen)
    138 except StopIteration:
    139     raise RuntimeError("generator didn't yield") from None

Cell In[12], line 5, in temporary_directory_change(path)
      2 @contextmanager
      3 def temporary_directory_change(path):
      4     original = os.getcwd()
----> 5     os.chdir(path)
      6     try:
      7         yield

FileNotFoundError: [WinError 2] The system cannot find the file specified: '/tmp'

Wrap-Up: What You Learned#

  • with calls enter on entry and exit on exit, guaranteed even when an exception occurs.
  • @contextmanager turns a single generator into a full context manager; before yield is enter, after yield is exit.
  • Wrapping yield in try/finally guarantees cleanup runs even if the with-block raises.
  • closing() adds context-manager behavior to any object that only has a close() method.
  • suppress() silently ignores only the specific exception types listed, nothing else.
  • redirect_stdout/redirect_stderr temporarily send output to any file-like object, like an in-memory buffer.
  • ExitStack manages a dynamic, runtime-determined number of context managers and plain cleanup callbacks.
  • nullcontext is a genuine do-nothing context manager for conditionally optional context managers.
  • Any @contextmanager-built object also works directly as a function decorator.
  • A real pattern: temporarily changing state (like a working directory) with guaranteed restoration.
  • That wraps up contextlib. Next up: dataclasses, for cutting down on class boilerplate.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.