Does entering a try block that never raises cost anything in CPython 3.14?
answer
- Something changed in 3.11
- Entering costs nothing; raising still does
- The compiler writes a static side table
- co_exceptiontable maps offsets to handlers
basics
~20 sEssentially nothing. Since CPython 3.11 the compiler emits no handler-setup instruction for a try block: the guarded ranges are recorded in a static table on the code object and are read only when an exception is actually propagating.
solid answer
~50 sUp to CPython 3.10, entering a `try` executed a setup instruction that pushed a handler entry onto the frame's block stack, so a `try` inside a hot loop paid a small tax on every iteration. Since 3.11 CPython uses the so-called zero-cost scheme: the compiler emits no handler-setup instruction for entering the guarded region - at most a `NOP` standing in for the `try:` line - and instead stores a side table on the code object (`co_exceptiontable`) mapping bytecode-offset ranges to a handler offset and the stack depth to restore. Nothing reads that table on the happy path; only a propagating exception searches it. So wrapping code in `try/except` is free when nothing raises, and the bookkeeping moved to the rare path. Two things people over-generalize: raising is still expensive, and a `with` statement still calls `__enter__` and `__exit__` on every entry.
code
python · 12 linesimport dis
def guarded(mapping, key):
try:
return mapping[key]
except KeyError:
return None
dis.dis(guarded)
print("table bytes:", len(guarded.__code__.co_exceptiontable))go deeper
Recall the headline: on modern CPython, a try block that never raises costs nothing at runtime, so do not avoid error handling for speed. Know that raising is the expensive part, not guarding.
Be ready to explain the mechanism: pre-3.11 a setup instruction pushed onto the frame's block stack on every entry; 3.11 replaced it with a static table on the code object read only while an exception propagates. Name 3.11 explicitly.
Show the measurement, not the slogan: disassemble both forms, time them with repeat-and-minimum, and state clearly which costs moved rather than vanished. Interviewers expect you to correct a teammate who hoists guards for speed on 3.14.
Own the guidance you give the codebase: after 3.11 the guard placement is a semantics decision, so reviews should argue about failure behaviour, not micro-cost, and any remaining exception hot spot is about how often you raise, not how often you guard.
## The question behind the question "Is `try/except` expensive?" is folklore-heavy, and the honest answer depends on which CPython you are talking about. An interviewer asking it wants to hear you separate **entering** a guarded region from **raising** through one, and to know which version changed the first of those. ## Before 3.11: the block stack Up to and including CPython 3.10, entering a `try` block executed a real bytecode instruction. It pushed an entry describing the handler onto the frame's *block stack* — a small runtime stack living on the frame alongside the value stack — and leaving the block popped it again. The push was cheap, a handful of pointer writes, but it was not nothing, and it happened on **every entry**. A `try` inside a loop that ran a million times paid it a million times, whether or not anything ever raised. That is where the advice "hoist the `try` out of the loop" and the general suspicion of defensive `try` blocks came from. ## Since 3.11: zero-cost exceptions CPython 3.11 replaced that scheme. The compiler now emits **no handler-setup instruction** for entering the guarded region - at most a `NOP` carrying the line number of the `try:` itself, which does no work. Instead it records, on the compiled code object, a compact static side table — you can print it as `co_exceptiontable` — that maps ranges of bytecode offsets to the offset of the handler that covers them, along with the stack depth to restore and a flag saying whether the instruction pointer must be pushed for a bare `raise` to re-raise correctly. On the successful path nothing ever consults that table. Only when an exception is raised does the interpreter take the offset it is currently executing, search the current frame's table for a range covering it, restore the stack and jump to the handler. If the frame has no covering range, the frame is unwound and the caller's table is searched, and so on up the stack. The name "zero-cost" refers precisely to this: **zero cost when no exception occurs**, at the price of a slightly more expensive raising path than the old block-stack scheme had. That is a good trade because the raising path is, by construction, the rare one. ## How to demonstrate it Two demonstrations land well in an interview. First, disassemble a guarded function and an unguarded one with `dis.dis`: the guarded version contains the same instructions for the body, with no setup instruction at the top - only that `NOP` marking the `try:` line - and `dis` prints an extra `ExceptionTable` section at the end listing the guarded ranges. Second, time both forms with `timeit` over enough iterations: on a guarded dictionary lookup the two differ by about a nanosecond per iteration, which is the dispatch of that single `NOP` and nothing more. Do both, because the disassembly proves the claim and the timing shows the claim is not defeated by something else. ## The three qualifications that separate an answer from a slogan **1. Raising is still expensive.** Zero cost applies to entry, not to the exception itself. Raising means instantiating the exception class, evaluating whatever message expression you wrote, allocating a traceback entry for every frame unwound, and matching the instance against each `except` clause. That is on the order of hundreds of nanoseconds to a microsecond — vastly more than the successful dictionary or attribute access it usually replaces. A loop that raises on most iterations is slow for that reason, not because of the `try`. **2. Cleanup code still runs.** "Zero cost" describes the *guarding machinery*. A `finally:` clause still executes its statements on the way out, and a `with` statement still performs two special-method calls, `__enter__` on entry and `__exit__` on exit, every single time. Do not generalize "`try` is free" into "context managers are free" — that is a common and visible mistake. **3. Nothing about the body changes.** The code inside the block runs exactly as it would outside it; there is no de-optimization, and no penalty for nesting several guarded regions. ## What it changes in real code On 3.11 and later there is no *performance* reason to avoid a `try` around a hot loop body, to hoist a `try` out of a loop, or to reach for a pre-check purely because you fear the guard. Hoist a `try` out of a loop when you want the **semantics** — one failure aborting the whole loop rather than being handled per item — and keep it inside when you want to continue. That decision is now purely about behaviour, which is exactly how you want a performance question to end: the runtime stopped charging you for the choice, so make it on meaning. ## Version summary Zero-cost try entry: CPython **3.11**. It is part of the same release's broader speedup work as the specializing adaptive interpreter (PEP 659) but is a separate mechanism — do not conflate them. On 3.14 everything above still holds, and the exception table is still visible on any code object.
- If entering a try is free, what is still expensive when an exception is actually raised?Instantiating the exception class and evaluating its message argument, allocating a traceback entry for every frame the exception unwinds through, searching each frame's exception table, and matching the instance against the `except` clauses. The cost therefore grows with the distance between the raise site and the handler, and with how much work the message expression does.
- Does the same zero-cost property apply to `finally` and to `with`?Entering the guarded region of a `try/finally` is free in the same way, but the `finally` body itself always executes, so its statements cost what they cost. A `with` statement is not free at all: every entry calls `__enter__` and every exit calls `__exit__`, which are ordinary method calls charged on each iteration.
- On 3.14, is there any performance reason left to hoist a try out of a loop?No. Hoisting is now purely a semantic decision: a `try` around the loop means the first failure ends the loop, while a `try` inside means each iteration is handled independently and the loop continues. Choose on the behaviour you want, and if the handler fires constantly, fix the frequency of raising rather than the placement of the guard.
Fire exits are drawn on the building's blueprint, not staffed at every doorway. Walking through the room costs nothing; only an actual fire sends someone to read the map.
saying these in an interview costs you the question
- Claims try/except always slows down the code it wraps
- Thinks zero-cost means raising an exception is also free
- Says exceptions are as cheap as returning a value
- Believes hoisting a try out of a loop speeds up 3.14
- Generalizes zero-cost try entry to with statements
- Cannot name the version where the change happened