Batu Lab NotesPractical developer guides

Use contextmanager to time a pure in-memory block

By Batu ยท English technical notes

Use contextmanager to time a pure in-memory block

Direct answer: record the start with perf_counter(), yield inside try, and append the elapsed value in finally. The finally branch runs when the with body raises as well as when it finishes normally.

The failing timer puts its recording statement after yield with no finally. The body calculates sum(range(1000)), asserts the known sum, and then deliberately raises ValueError. That exception resumes the generator by throwing at yield, so the later append is skipped. Its event list remains empty.

The corrected @contextmanager surrounds yield with try/finally. It runs the same pure in-memory calculation and deliberate exception, catches the expected exception outside the with, then checks that exactly one elapsed value was recorded and that it is non-negative. The printed output uses only event counts, not a measured duration, so it remains exact even though perf_counter() is inherently variable.

contextmanager converts a generator function into a context manager; its documentation explains that an exception from the body is reraised at the yield point and can be handled by finally. perf_counter() and contextmanager are available in all supported Python 3 versions. This fixture tests cleanup control flow, not timing accuracy or benchmark quality.

AI-assistance disclosure: Batu Lab Notes used AI assistance to draft this synthetic example and explanation.

Example

from contextlib import contextmanager
from time import perf_counter


@contextmanager
def broken_timer(events):
    started = perf_counter()
    yield
    events.append(perf_counter() - started)


@contextmanager
def timer(events):
    started = perf_counter()
    try:
        yield
    finally:
        events.append(perf_counter() - started)


broken_events = []
try:
    with broken_timer(broken_events):
        assert sum(range(1000)) == 499500
        raise ValueError("exercise failure path")
except ValueError:
    pass

elapsed_events = []
try:
    with timer(elapsed_events):
        assert sum(range(1000)) == 499500
        raise ValueError("exercise failure path")
except ValueError:
    pass

assert broken_events == []
assert len(elapsed_events) == 1
assert elapsed_events[0] >= 0
print(f"broken events: {len(broken_events)}")
print(f"corrected events: {len(elapsed_events)}")

Expected output:

broken events: 0
corrected events: 1