Hands-on contextlib.ExitStack vs nested with and manual close: real cycles/s for N resources, measured on Linux localhost in this hands-on lab for SREs.
3 min read
Manage N context-managed resources via contextlib.ExitStack vs fixed nested with vs manual try/finally close. This lab reports cycles/s on Linux localhost.
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. Dummy resources + TemporaryFile. Differentiates from lab 60 — this is dynamic N.
Verdict up front (N=4): manual close ~1056421 cycles/s; nested with ~959372; ExitStack ~253269. At N=64, ExitStack ~20877 vs manual ~58776.
| Arm | Pattern |
|---|---|
ExitStack.enter_context | dynamic N |
nested with a,b,c,d | fixed N=4 |
try/finally .close() | manual |
ExitStack.callback | close hooks |
| TemporaryFile ExitStack vs manual | real FDs |
Script: lab-evidence/121-exitstack-vs-nested-with/results/run_lab.py.
| N | ExitStack | manual close | nested with |
|---|---|---|---|
| 4 | 253269 | 1056421 | 959372 |
| 16 | 120396 | 338967 | — |
| 64 | 20877 | 58776 | — |
| N | ExitStack TemporaryFile | manual TemporaryFile |
|---|---|---|
| 4 | 7837 | 8352 |
| 16 | 2595 | 2685 |
| 64 | 533 | 660 |
OS file costs dominate — ExitStack overhead shrinks relatively.
You cannot write a statically nested with for len(files) known only at runtime. ExitStack exists for that: push contexts as you open, unwind safely on error.
Resources exit in reverse enter order — same as nested with. Register releases immediately after successful acquire.
with is fine and faster here.The win for ExitStack is not the happy-path cycles/s table — it is cleaning up when the 7th of 16 opens fails. Manual lists need careful finally; nested with cannot express variable depth. ExitStack keeps LIFO semantics without a hand-built stack.
callback(close) does not enter a context manager’s __enter__. Prefer enter_context for real CM objects; use callbacks for plain close functions or logging. Mixing both is fine when documented.
TemporaryFile arms show kernel work dwarfing ExitStack bookkeeping (thousands of cycles/s, not hundreds of thousands). Optimize FD count and directory placement before micro-tuning ExitStack vs manual close.
enter_context / inconsistent callbacks.Evidence: summary.json, summary.txt.
Pair ExitStack with typed helpers (open_all(paths)) so call sites stay short and acquire/release policy stays in one module.
One Linux box. Synthetic DummyResource + local TemporaryFile. Not thread pools.
Keep the comparison honest: measure your resource type (dummy vs FD) before optimizing.
For N=4, nested/manual ~1056421 cycles/s beat ExitStack ~253269. Prefer ExitStack when N is dynamic — correctness under exceptions beats a fixed-with microbench.
Lab evidence
Ran the resource cleanup benchmark on Linux localhost with Python 3.13.5. Measured 7 rounds at N=4, 16, and 64 using p50 cycles/s, with dummy resources and TemporaryFile checks. At N=4, manual close measured about 1,056,421 cycles/s, nested with 959,372, and ExitStack 253,269; at N=64, ExitStack was about 20,877 versus manual 58,776. The dynamic-N exception cleanup tradeoff was the key finding.
Hands-on gc.collect cost empty vs cyclic garbage lab: real collect latency plus reclaim counts, measured on Linux localhost today in this lab for SREs.
Hands-on1 Oct 20264 min read
Hands-on itertools.batched vs manual list-slice chunking: real items/s batching sequences, measured on Linux localhost today in this hands-on lab for SREs.
Hands-on1 Oct 20263 min read
Hands-on sqlite3 vs shelve local KV store lab: real insert/get ops/s plus file sizes, measured on Linux localhost today in this hands-on lab for SREs.
Hands-on1 Oct 20264 min read