4 min read
Is contextlib.closing / ExitStack “free” compared with a manual try / finally? This lab times file-like open → read → close patterns on Linux localhost: native with, closing(), @contextmanager, and ExitStack vs bare try/finally.
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. N=50,000 open/close cycles per arm. FakeFile stands in for objects that expose close() but may lack __enter__ (the closing() use case). Affiliates: 0. Not a correctness substitute — context managers still win for nested cleanup and exceptions.
Verdict up front: bare try/finally ~8.3M/s; native with ~4.3M (~1.9× slower); closing() ~2.9M (~2.8×); ExitStack + one closing ~0.54M (~15×). Prefer clarity; pay ExitStack only when you need dynamic stacks.
| Arm | Pattern |
|---|---|
| try/finally | f=FakeFile(); try: read; finally: close |
native with | FakeFile implements __enter__/__exit__ |
closing() | with closing(FakeFile()) |
@contextmanager | generator CM wrapping FakeFile |
| ExitStack ×1 / ×3 | enter_context(closing(...)) |
| try/finally ×3 | three files, reverse close |
| BytesIO | try/finally vs closing(BytesIO) |
Script: lab-evidence/60-contextlib-vs-try-finally/results/run_lab.py.
| Arm | ops/s | ns/op |
|---|---|---|
| try/finally | 8,323,021 | 120.1 |
native with | 4,270,934 | 234.1 |
closing() | 2,933,460 | 340.9 |
@contextmanager | 1,132,779 | 882.8 |
| ExitStack ×1 | 544,158 | 1837.7 |
| try/finally ×3 | 2,672,477 | 374.2 |
| ExitStack ×3 | 417,639 | 2394.4 |
| Arm | ops/s | ns/op |
|---|---|---|
| BytesIO try/finally | 12,952,516 | 77.2 |
BytesIO closing() | 3,507,039 | 285.1 |
Same shape: try/finally ~3.7× closing() on this path.
closing() per cycle. You will not see this on a disk read that takes milliseconds.ExitStack is for dynamic / many resources — here one-slot ExitStack was ~15× slower than try/finally; three-resource still ~6.4×. Worth it when resource count is data-driven.with beats closing() (~1.46×) when the object already implements the CM protocol — prefer implementing __enter__/__exit__ over wrapping.@contextmanager sits between closing and ExitStack (~883 ns) — generator setup shows up in a tight loop.The microbench ranks bare try/finally first because it does the least work. Production code usually values exception-safe nested cleanup, readable scopes, and libraries that only expose close(). Paying a few hundred nanoseconds per enter/exit is rational next to any real I/O. Strip wrappers only after a profiler names the CM path.
closing() on an object that already supports with — double-wrap noise.| Need | Prefer |
|---|---|
| One resource, hottest path | try/finally or native with |
Object has close() only | contextlib.closing |
| Dynamic / variable count | ExitStack |
| Custom setup/teardown | @contextmanager / CM class |
Evidence: /workspace/lab-evidence/60-contextlib-vs-try-finally/results/.
Correctness first; nanoseconds second. On this box try/finally hit ~8.3M/s vs closing() ~2.9M (~2.8×) and ExitStack-one ~15× behind. Use contextlib for safety and nesting; only strip it after a profiler points at the enter/exit path.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5; N=50000. try/finally FakeFile 8.32M vs closing 2.93M (~2.84x); ExitStack one ~0.54M (~15.3x slower than try/finally); three-resource try/finally vs ExitStack ~6.4x. Affiliates: 0. Evidence: lab-evidence/60-contextlib-vs-try-finally/.
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read
Hands-on signal SIGUSR1 vs threading.Event wakeup lab: real p50 latency in microseconds, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read
Hands-on cmath vs math.hypot magnitude ops lab: real ops/s for abs, polar, and phase, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20263 min read