ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
Topics
sre111localhost lab102performance70python58javascript47bundle size36benchmarking35node.js35web development21typescript19benchmark16content creation15All topics
About
Loading article…

ShopperCove

Hands-on notes on developer tools, written after the tool has actually been run.

All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — FakeFile (p50)
  5. BytesIO check
  6. Reading it
  7. Why this still prefers context managers in prod
  8. Pitfalls
  9. When to pick what
  10. Reproduce
  11. Closing
  1. Blog

contextlib vs try/finally: Cleanup Lab

30 September 2026·4 min read

Summary
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — FakeFile (p50)
  5. BytesIO check
  6. Reading it
  7. Why this still prefers context managers in prod
  8. Pitfalls
  9. When to pick what
  10. Reproduce
  11. Closing

Intro — what this post promises

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:

  • 01shoppercove.comThreadPoolExecutor vs sequential localhost lab
  • 02shoppercove.comitemgetter vs lambda sort localhost lab
  • 03shoppercove.comperf_counter vs time localhost lab
  • 04shoppercove.comlogging vs print localhost lab
  • 05shoppercove.comcopy vs deepcopy localhost lab
  • 06shoppercove.comlru_cache hit vs miss localhost lab
  • 07shoppercove.comitertools vs python loops localhost lab
  • 08shoppercove.comarray vs list ints localhost lab

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.


Arms

ArmPattern
try/finallyf=FakeFile(); try: read; finally: close
native withFakeFile implements __enter__/__exit__
closing()with closing(FakeFile())
@contextmanagergenerator CM wrapping FakeFile
ExitStack ×1 / ×3enter_context(closing(...))
try/finally ×3three files, reverse close
BytesIOtry/finally vs closing(BytesIO)

Lab topology

N = 50000 cycles/arm; repeats=9; metric = p50 ops/s
FakeFile: __slots__, read(), close(); optional CM protocol

Script: lab-evidence/60-contextlib-vs-try-finally/results/run_lab.py.


Lead table — FakeFile (p50)

Armops/sns/op
try/finally8,323,021120.1
native with4,270,934234.1
closing()2,933,460340.9
@contextmanager1,132,779882.8
ExitStack ×1544,1581837.7
try/finally ×32,672,477374.2
ExitStack ×3417,6392394.4

BytesIO check

Armops/sns/op
BytesIO try/finally12,952,51677.2
BytesIO closing()3,507,039285.1

Same shape: try/finally ~3.7× closing() on this path.


Reading it

  • Protocol cost is real but small in absolute ns — ~120 ns try/finally vs ~341 ns 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.
  • Native 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.

Why this still prefers context managers in prod

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.


Pitfalls

  1. Microbenching cleanup and skipping I/O — the close path is rarely your bottleneck.
  2. Hand-rolling nested finally — easy to leak on partial failure; ExitStack exists for a reason.
  3. closing() on an object that already supports with — double-wrap noise.
  4. Assuming ExitStack is “heavy” in wall-clock APIs — microseconds vs network RTT.

When to pick what

NeedPrefer
One resource, hottest pathtry/finally or native with
Object has close() onlycontextlib.closing
Dynamic / variable countExitStack
Custom setup/teardown@contextmanager / CM class

Reproduce

python3 lab-evidence/60-contextlib-vs-try-finally/results/run_lab.py

Evidence: /workspace/lab-evidence/60-contextlib-vs-try-finally/results/.


Closing

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.

contextlibclosingexitstacktry/finallycontext managerpythonlocalhost labsre

Lab evidence

What I found running this

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/.

Notes when a lab post goes up

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

Keep reading

  • platform vs os.uname Inventory: Localhost Lab

    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

  • signal vs threading.Event Wakeup: Localhost Lab

    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

  • cmath vs math.hypot Magnitudes: Localhost Lab

    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