Plate 16
ExitStack vs Nested with Resources: Localhost Lab
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.
Aditya Challa3 min read
Intro — what this post promises
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:
- itertools batched vs chunk localhost lab
- graphlib topo vs manual localhost lab
- functools cache vs lru localhost lab
- tomllib vs json localhost lab
- path glob vs fnmatch localhost lab
- dataclass replace vs manual localhost lab
- zoneinfo vs utc offset localhost lab
- heapq merge vs sorted localhost lab
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.
Arms
| 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 |
Lab topology
Script: lab-evidence/121-exitstack-vs-nested-with/results/run_lab.py.
Lead table — dummy resources (p50 cycles/s)
| N | ExitStack | manual close | nested with |
|---|---|---|---|
| 4 | 253269 | 1056421 | 959372 |
| 16 | 120396 | 338967 | — |
| 64 | 20877 | 58776 | — |
Temp files (FD reality)
| N | ExitStack TemporaryFile | manual TemporaryFile |
|---|---|---|
| 4 | 7837 | 8352 |
| 16 | 2595 | 2685 |
| 64 | 533 | 660 |
OS file costs dominate — ExitStack overhead shrinks relatively.
Dynamic N story
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.
LIFO cleanup
Resources exit in reverse enter order — same as nested with. Register releases immediately after successful acquire.
Reading it
- Variable resource counts → ExitStack.
- Fixed tiny N → nested
withis fine and faster here. - Manual close is fast but easy to leak under exceptions.
- Not a substitute for structured concurrency task groups.
Exception paths
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.
Callbacks vs enter_context
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.
FD-bound workloads
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.
Pitfalls
- Forgetting
enter_context/ inconsistent callbacks. - Nesting ExitStacks without need.
- Comparing to lab 60 without dynamic-N framing.
- Blaming ExitStack when TemporaryFile I/O dominates.
Reproduce
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.
Limits
One Linux box. Synthetic DummyResource + local TemporaryFile. Not thread pools.
Keep the comparison honest: measure your resource type (dummy vs FD) before optimizing.
Takeaway
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
What I found running this
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.
Related links
Plate 68
gc.collect Cost Empty vs Cycles: Localhost Lab
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.
Observability & SRE · 1 Oct 2026
Plate 40
itertools.batched vs Manual Chunking: Localhost Lab
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.
Observability & SRE · 1 Oct 2026
Plate 28
sqlite3 vs shelve Local KV: Localhost Lab
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.
Observability & SRE · 1 Oct 2026