S
Saurav Danej
90-Day AI/ML LinkedIn Content System
← All days
12
Day 12 of 90Python

Context managers — the with-statement, demystified

POST 1 of 5 MorningPythonConcept

with-statements never leak resources

Every long-lived program eventually leaks something. File handles, network sockets, database connections, locks, GPU memory. The leak is rarely a single bug — it's the accumulation of forgotten cleanup. Open a thing on line 12; forget to close it on line 47; the leak waits.

Python's 'with' statement was designed to make this class of bug nearly impossible.

with open('file') as f:
    process(f)

The runtime guarantees that f.close() runs when the block exits. Even if process(f) raises an exception. Even if you return early from a function. Even if a deeply nested call inside process() causes a stack unwind. The cleanup happens.

Behind the scenes, 'with' looks for two methods on the object — __enter__ and __exit__. __enter__ returns the resource (and runs setup). __exit__ runs at block end (and handles teardown). Python ensures __exit__ is called even on exceptions.

This pattern generalises far beyond files:

Database connections. with engine.begin() as conn: ... — transaction commits on success, rolls back on exception.

Locks. with lock: ... — acquire on enter, release on exit. Even if the body crashes, the lock releases.

Temp directories. with tempfile.TemporaryDirectory() as path: ... — directory created on enter, deleted on exit.

GPU memory. with torch.cuda.device(0): ... — device set on enter, restored on exit.

Timers and metrics. with timer('inference'): ... — start time recorded on enter, duration computed on exit.

The principle is simple — any resource with a 'must clean up' rule belongs in a context manager. If you find yourself writing try/finally with a paired open/close, you almost certainly want a context manager instead.

Leaks are bugs the runtime can prevent for you. Let it.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonContextManagers
POST 2 of 5 MiddayPythonDeep dive

Two ways to write a context manager

Python gives you two patterns to write a context manager. They're equivalent in capability; they differ in style and in what they're best at.

Pattern one — class with __enter__ and __exit__:

class Timer:
    def __enter__(self):
        self.start = time.time()
        return self  # what 'as' binds to
    def __exit__(self, exc_type, exc_value, tb):
        print(f'took {time.time() - self.start:.2f}s')
        return False  # don't suppress exceptions

Use the class form when:
- You need to maintain instance state across enter/exit (timer state, counters)
- You want to handle exceptions explicitly inside __exit__ (log, transform, suppress)
- You need an async sibling (__aenter__ / __aexit__)
- The context manager has additional methods callers should call inside the block

Pattern two — the @contextmanager decorator:

from contextlib import contextmanager

@contextmanager
def timer():
    start = time.time()
    try:
        yield
    finally:
        print(f'took {time.time() - start:.2f}s')

The code BEFORE yield runs at __enter__. The code AFTER yield (inside finally) runs at __exit__. Whatever you yield is what 'as' binds to. The try/finally ensures cleanup happens even on exception.

Use the decorator form when:
- The context manager is short and doesn't need much state
- You're writing a one-off CM for a specific function
- The setup/teardown reads as a small linear flow

My default is the decorator. It's shorter, fewer moving parts, and reads top to bottom. I switch to the class form when I need explicit exception handling or async support.

For async, the equivalents are __aenter__/__aexit__ and @asynccontextmanager. Same shape, just async.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonAdvanced
POST 3 of 5 AfternoonPythonCode

Time any block of code with a context manager

Here's a 10-line context manager you'll use across the next 78 days, and probably every Python project after that.

@contextmanager turns the function into a CM. Setup happens before yield. Cleanup happens after yield (inside finally so it runs even if the body raises).

Usage:

with timed('inference'):
    out = model.predict(x)

Prints 'inference: 12.3 ms'. Drop it around any block of code you suspect might be slow. Inside training loops, around model loads, around HTTP calls. Way more useful than scattering time.time() calls and arithmetic across your code.

Note the structure of __exit__'s arguments. If we'd written this as a class, __exit__ would receive (exc_type, exc_value, tb). The contextmanager decorator handles those for you — exceptions inside the 'with' block automatically propagate out unless you explicitly catch them around the yield.

If you wanted to suppress exceptions (rarely correct, but occasionally useful), you'd add a try/except around yield. If you wanted to log them, same. If you wanted to add fields to a tracing span based on whether the block raised, also same.

Why I always print durations to stderr in production code: stdout is for program output, stderr is for diagnostics. Use logger.info() if you have a logger configured. Don't pollute pipeable program output with timing noise.

The @contextmanager decorator + a tiny try/finally is one of the most underrated patterns in Python. Once you start writing CMs, you'll find places to use them everywhere — feature flags scoped to a block, mock setups in tests, profiler regions, database transactions. Pattern's the same.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonCode
POST 4 of 5 EveningPythonTip

ExitStack — composing many context managers

Sometimes you need to enter several context managers at once, and the count is only known at runtime. Maybe you're processing N files specified on the command line. Maybe you're holding M database connections for a fan-out query.

The naive approach is nesting:

with open(p1) as f1:
    with open(p2) as f2:
        with open(p3) as f3:
            ...

Ugly. And impossible if you don't know the count up front.

contextlib.ExitStack solves this:

from contextlib import ExitStack

with ExitStack() as stack:
    files = [stack.enter_context(open(p)) for p in paths]
    process(files)

ExitStack is itself a context manager. Inside its block, you call enter_context() to register additional context managers. When the ExitStack exits, it calls __exit__ on every registered manager in reverse order of entry — last in, first out. Just like nested 'with' blocks would.

Where this shines:

Dynamic resource counts. N files, N connections, N temp directories. Loop and stack.enter_context() each one.

Conditional context managers. Sometimes you want a CM, sometimes you don't. Ugly with nested 'with'; clean with ExitStack and an if-statement around stack.enter_context().

Test fixtures composing several mocks/patches/temp resources whose count varies per test.

Multi-step ETL pipelines that hold open multiple readers and writers.

The register variant — stack.callback(fn, *args, **kwargs) — registers an arbitrary cleanup function instead of a CM. Useful for paired setup/teardown that doesn't naturally fit a context manager protocol.

ExitStack is in the standard library. No external dependency. It's the right answer any time 'how many context managers do I need' is the question.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonTips
POST 5 of 5 NightPythonRecap

Day 12 — never write try/finally by hand again

End of Day 12. Halfway through week two. Python's standard library is delivering more than tutorials credit it for.

What we covered.

Morning, the principle behind 'with' — guaranteed cleanup, even on exception, even on early return, even on deep stack unwinds. Any resource with a 'must clean up' rule belongs in a context manager.

Midday, the two ways to author a context manager — class with __enter__/__exit__, or @contextmanager-decorated function. Both are equivalent in capability. The decorator is cleaner for one-offs; the class is better when you need explicit exception handling or instance state.

Afternoon, a 10-line @timed context manager. Drop it around any block to measure duration. Endlessly reusable. Way better than scattering time.time() calls.

Evening, ExitStack for the cases where you need a runtime-decided number of context managers. Stack as many as you need, get reverse-order cleanup automatically. Standard library. Free.

A broader pattern for the week. Python's elegance often hides in modules with terse names — contextlib, functools, itertools, collections. Each one is a small toolkit of patterns that the standard library hardened over decades. Reach for them before reinventing.

Tomorrow, Day 13, type hints and dataclasses. The combination that turned Python from 'great for prototypes' into 'production-viable for big projects'. Type hints don't run, but they catch entire classes of bug before runtime, and they make every framework's auto-complete useful. Dataclasses replace 30-line boilerplate with a 5-line declaration.

Two days left in week two. We're about to wrap the Python core and head into DSA territory.

See you in the morning.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonContextManagers