Why can contextlib.suppress cost more than try/except pass in a hot loop?
answer
- A with statement is never free
- Two protocol methods run every iteration
- __enter__ and __exit__ are Python-level calls
- try entry emits no bytecode since 3.11
- Suppression exits the whole block
basics
~20 sBecause a with statement pays on every entry: it calls the suppressor's enter and exit, both ordinary Python-level methods, while entering a try block has emitted no handler-setup instruction since CPython 3.11 and costs nothing unless something raises.
solid answer
~40 s`contextlib.suppress(KeyError)` is a context manager object, so each iteration of the `with` runs two Python-level method calls — `__enter__` on the way in and `__exit__` on the way out, where the class check against the suppressed types happens — plus an instance construction if you build it inside the loop. A `try/except KeyError: pass` pays nothing on entry since 3.11 and only costs when it actually catches. The gap is small, tens of nanoseconds an iteration, so it matters only in a genuinely hot loop and you should confirm it with `timeit` before acting. Because `contextlib.suppress` is documented as reentrant you can hoist one instance out of the loop, which removes the construction but not the two calls. Outside hot paths, prefer whichever reads better — and remember the semantics differ.
code
python · 24 linesimport timeit
SETUP = """
import contextlib
budgets = {'svc-1': 12.5}
skip = contextlib.suppress(KeyError)
"""
WITH_SUPPRESS = """
with skip:
budgets['svc-1']
"""
PLAIN_TRY = """
try:
budgets['svc-1']
except KeyError:
pass
"""
n = 500_000
for label, stmt in (("suppress", WITH_SUPPRESS), ("try", PLAIN_TRY)):
best = min(timeit.repeat(stmt, SETUP, repeat=5, number=n))
print(label, f"{best / n * 1e9:.1f} ns/op")go deeper
Know that contextlib.suppress swallows the named exception and that doing so leaves the whole with block, so keep such a block to a single statement. The cost angle is not expected of you yet.
Explain the mechanism: a with statement calls enter and exit on every entry, while entering a try emits no handler-setup instruction since 3.11, so the helper carries a small fixed per-iteration cost - on the order of forty nanoseconds - that the plain form does not.
Give the magnitude and the measurement before the verdict, and steer the discussion to the block-exit semantics, which decide the choice far more often than the nanoseconds do.
Set a style rule that scales: readability helper by default, plain try in measured hot paths, and no blanket ban justified by a micro-benchmark nobody ran on the code in question.
## The two forms are not equivalent in cost `contextlib.suppress(KeyError)` and `try: ... except KeyError: pass` express the same intent, and people reasonably assume the stdlib helper is free. It is not, and the reason is a nice test of whether you understand what the `with` statement compiles to versus what `try` compiles to on a modern CPython. **`try` entry is free.** Since 3.11 the compiler emits no handler-setup instruction for entering a guarded region; the handler ranges live in a static table on the code object and are consulted only while an exception is propagating. A `try/except` that never catches costs literally nothing at runtime. **`with` entry is not free.** `contextlib.suppress` is an ordinary class. A `with` statement over an instance of it looks up and calls `__enter__` on entry and `__exit__` on exit, unconditionally, on every pass. Both are Python-level functions in the standard library, so each iteration pays two full function calls — frame setup, argument passing, return — before any of your code runs. `__exit__` additionally receives the exception type, value and traceback (all `None` when nothing raised) and performs a class check to decide whether to swallow. On top of that, the usual spelling constructs the manager inline — `with suppress(KeyError):` — so a fresh instance is allocated on every iteration too. ## How big is the difference? Small. Tens of nanoseconds per iteration on a current build: two Python-level calls plus an allocation, against zero. That is irrelevant in almost all code and visible in a loop running millions of times where the body itself is a single dictionary lookup — exactly the situation where a reviewer notices. Measure it with `timeit` rather than asserting it: time both spellings with the same loop count, take the minimum of several repeats, and divide by the count to get nanoseconds per operation. If the gap is smaller than the spread between your repeats, there is nothing to talk about. ## Hoisting the instance `contextlib.suppress` is documented as **reentrant**, which means a single instance may be used in more than one `with` statement, including nested ones, without breaking. So you may legitimately build it once at module level and reuse it in the loop. That removes the per-iteration construction but not the two method calls, so it recovers part of the gap and never all of it. Do not assume this of context managers in general: most are single-use, and reusing one that keeps state is a bug, not an optimization. ## The semantic difference that matters more than the cost If you take one thing from this question, take this: the two forms do not do the same thing. A suppressed exception **exits the entire `with` block**. Statements after the failing one inside the block never run, and control resumes after the block. So this: ```python with suppress(KeyError): total += budgets['svc-1'] seen.add('svc-1') ``` silently skips the `seen.add` when the lookup fails, which is almost never what the author meant. Keeping the block to a single statement is the discipline that makes `suppress` safe, and it is also what makes it read well: one line whose failure you genuinely do not care about. ## When each one is right Reach for `contextlib.suppress` when it improves readability at a place that is not hot: removing a file that may not exist, closing something that may already be closed, one expression whose failure is genuinely uninteresting. It reads as a declaration of intent, and it cannot accidentally grow a handler body that does real work — a `pass` handler can. Use `try/except` when you are in a hot loop, when you want statements after the failure to still run, when the handler needs to do anything at all (log, count, substitute a value), or when you want to bind the exception with `as` and inspect it. `suppress` deliberately gives you no access to the exception it swallowed. ## How to answer this in an interview The strong answer has three beats and refuses to overclaim. First the mechanism: `with` pays two Python-level protocol calls per entry while `try` entry compiles to nothing since 3.11. Then the magnitude: tens of nanoseconds, so it only matters in a measurably hot loop, and here is how I would measure it. Then the redirection: the real reason to choose between them is the block-exit semantics and readability, not the nanoseconds. A candidate who declares `suppress` "slow" and bans it has lost the plot as surely as one who insists a `with` statement is free.
- Is it safe to build one contextlib.suppress instance and reuse it inside a loop?Yes. `contextlib.suppress` is documented as reentrant, so one instance can serve many `with` statements, including nested ones. Hoisting it removes the per-iteration construction but not the two protocol calls, so it closes part of the gap. Do not generalize the trick: most context managers are single-use and reusing a stateful one is a bug.
- What happens to statements after the failing one inside a suppress block?They are skipped. Suppressing the exception unwinds out of the entire `with` block and resumes after it, so any later statement in the block never runs. That is why a `suppress` block should normally contain exactly one statement — otherwise you silently skip work whose omission nobody intended.
- When would you prefer suppress even though try/except is cheaper?Whenever the path is not hot and the intent is genuinely "this one operation may fail and I do not care": deleting a file that may be absent, closing something already closed. It states the intent in one line and cannot quietly grow into a handler that does real work. If you need to log, count, substitute a value or inspect the exception, use `try/except` instead.
saying these in an interview costs you the question
- Assumes a stdlib helper cannot cost anything
- Thinks with and try compile to the same thing
- Says suppress resumes after the failing statement
- Bans suppress everywhere over tens of nanoseconds
- Cannot say how to measure the difference
- Expects access to the exception suppress swallowed