Why is the name in `except ValueError as err` undefined after the except block?
answer
- The binding does not outlive the handler
- The compiler adds something after the body
- Refcounting cannot free a cycle
- Exception, traceback, frame, locals, exception
- An implicit del ends the handler
basics
~20 sPython 3 compiles the handler as if it ended with del err, so the binding is gone once the block exits — breaking the cycle from the exception through its traceback to the frame. Copy it elsewhere to keep it.
solid answer
~40 sSince Python 3.0 the `as` target of an `except` clause is deleted when the handler finishes: the compiler wraps the handler body in the equivalent of `try: <body> finally: del err`. The reason is memory. The exception instance holds its traceback in `__traceback__`, the traceback references the frame that is handling it, and the frame's locals reference the exception — a cycle that would keep the whole frame, and everything its locals point at, alive until the cycle collector ran. Deleting the name breaks it deterministically. Two consequences bite in practice: touching `err` after the block raises `NameError`, and the deletion also removes any *pre-existing* binding of that same name. If you need the exception later, assign it to a different name inside the handler.
code
python · 10 lineserr = "something I care about"
try:
raise ValueError("boom")
except ValueError as err:
print("inside:", err)
try:
print(err)
except NameError as problem:
print("after the block:", problem)go deeper
Just remember the rule and the workaround: the handler's as name stops existing once the block ends, so if you need the exception afterwards, copy it to another variable inside the block.
Be able to describe the compiled shape — the body wrapped in a try/finally that deletes the name — and the reference cycle through __traceback__ and the frame that motivates it.
Connect it to real retention: explain why keeping caught exceptions on long-lived structures pins entire call stacks, and what you store instead when a service needs to report on errors after the fact.
Frame it as a policy question — what error context a system retains, for how long, and in what form, so diagnostics stay useful without turning the error path into a memory growth path.
This is one of Python 3's small, deliberate surprises, and it has a concrete reason behind it. ## What actually happens When you write ```python try: ... except ValueError as err: log(err) ``` the compiler does not simply bind `err` for the duration of the block. It generates roughly this: ```python except ValueError as err: try: log(err) finally: del err ``` The name is unbound on the way out of the handler — on the normal path, on the exception path, and on a `return` out of the handler alike. Reading `err` after the statement therefore raises `NameError: name 'err' is not defined`. This behaviour arrived with Python 3.0 (PEP 3110, which also changed the syntax from the Python 2 comma form to `as`) and is unchanged through 3.14. ## Why the language does this The motive is a reference cycle, and it is a big one. In Python 3 every exception instance carries its own traceback on the `__traceback__` attribute. A traceback object references the frame objects it walked through. A frame object references its locals. If the handler's local `err` holds the exception, you get: > exception -> `__traceback__` -> frame -> locals -> `err` -> exception A cycle. CPython's primary memory management is reference counting, and reference counting cannot reclaim a cycle; only the cyclic garbage collector can, and it runs when it runs. Left alone, one caught exception would pin its entire frame — every local variable in the function, including whatever large objects were in flight — until a collection happened to sweep it up. In a function that handles exceptions in a loop, that is a real and surprising retention pattern. Deleting the name at the end of the handler breaks the cycle at a deterministic point, and the frame becomes reclaimable by refcounting alone. ## The consequence people trip over The deletion is unconditional. It does not restore a previous value; it removes the binding: ```python err = "something I care about" try: raise ValueError("boom") except ValueError as err: pass print(err) # NameError, and the original string is gone ``` This is why the convention is to use a short, throwaway name such as `err` or `exc` in a handler and never a name that means something in the surrounding function. ## How to keep the exception Bind it to a different name inside the block. The exception object itself is not destroyed — only the handler's binding to it is: ```python saved = None try: raise ValueError("boom") except ValueError as err: saved = err print(saved) # ValueError('boom') print(saved.__traceback__ is not None) # True ``` Be aware that this reinstates exactly the retention the language was trying to avoid, only now under your control: `saved` keeps the traceback, which keeps the frames, which keeps their locals. That is fine for a value you are about to inspect and drop, and it is a leak if you stash it on a long-lived object such as a queue, a cache or a module-level list. If all you need is the message or the type, extract that instead of holding the instance: ```python except ValueError as err: reason = str(err) ``` Another common approach is to avoid the problem entirely: do the work that needs the exception *inside* the handler, where the binding is still live. ## Related, but not the same - Re-raising does not need the name at all. A bare `raise` inside the handler re-raises the exception being handled without referring to any binding, so the deletion never gets in its way. - The exception instance is also reachable from `sys.exc_info()` while the handler is running, and that state is cleared when the handler exits too — for the same kind of reason. ## What an interviewer is really checking Nobody's offer turns on this detail, but it is a good window into whether you understand CPython's memory model. A candidate who can say "the exception holds the traceback, the traceback holds the frame, the frame holds the exception, so the compiler cuts the link" is telling you they know how reference counting and cycles actually work, which is worth far more than the trivia itself.
- What is the memory cost of stashing a caught exception instance on a long-lived object?The instance holds `__traceback__`, which holds the frames it unwound through, which hold every local in those frames. Keeping the instance on a module-level list, a cache or a queue therefore pins whole call stacks' worth of objects. Store the type, the message or a formatted string instead, and keep the instance only for as long as you are actively inspecting it.
- Does the deletion interfere with re-raising the exception later in the handler?No. A bare `raise` inside the handler re-raises the exception currently being handled from the interpreter's own exception state, not from the `as` name, so it works regardless. The deletion only happens once the handler body has finished, so `raise err` inside the block is fine too.
- Why does Python delete the name instead of just restoring whatever it held before?Because the clause is compiled as a plain binding plus a `del`, not as a save-and-restore scope. Python has no block scoping for local names — a function has one namespace — so there is nothing to restore to. The pragmatic answer is to treat handler targets as throwaway names and never reuse a meaningful one.
The handler's name is a visitor badge: it works while you are inside the room and is taken off you at the door, because leaving it on would keep the whole room booked.
saying these in an interview costs you the question
- Thinks the exception object itself is destroyed
- Says the name survives if the handler assigns to it
- Claims the previous value of the name is restored
- Believes this is only a style convention, not compiled behaviour
- Cannot name the cycle: exception, traceback, frame, locals