ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 16

  1. Blog
  2. /Observability & SRE

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 Challa·1 October 2026·3 min read

Hands-on
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — dummy resources (p50 cycles/s)
  5. Temp files (FD reality)
  6. Dynamic N story
  7. LIFO cleanup
  8. Reading it
  9. Exception paths
  10. Callbacks vs enter\_context
  11. FD-bound workloads
  12. Pitfalls
  13. Reproduce
  14. Limits
  15. Takeaway

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

ArmPattern
ExitStack.enter_contextdynamic N
nested with a,b,c,dfixed N=4
try/finally .close()manual
ExitStack.callbackclose hooks
TemporaryFile ExitStack vs manualreal FDs

Lab topology

N in {4, 16, 64} · 500 cycles · 7 rounds · p50
metric: cycles/s = INNER / p50_s

Script: lab-evidence/121-exitstack-vs-nested-with/results/run_lab.py.


Lead table — dummy resources (p50 cycles/s)

NExitStackmanual closenested with
42532691056421959372
16120396338967—
642087758776—

Temp files (FD reality)

NExitStack TemporaryFilemanual TemporaryFile
478378352
1625952685
64533660

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 with is 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

python3 lab-evidence/121-exitstack-vs-nested-with/results/run_lab.py

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.

pythonexitstackcontextmanagerbenchmarkingperformanceresource managementconcurrency

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.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

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

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — dummy resources (p50 cycles/s)
  5. Temp files (FD reality)
  6. Dynamic N story
  7. LIFO cleanup
  8. Reading it
  9. Exception paths
  10. Callbacks vs enter\_context
  11. FD-bound workloads
  12. Pitfalls
  13. Reproduce
  14. Limits
  15. Takeaway
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove