Question presented to candidate: "A function four calls deep raises a ValueError, and only the top-level function has a try/except. Walk me through exactly what Python does between the raise and the handler. What runs on the way up, and where can the exception get stopped or lost?"
What a strong answer should cover:
- Raising stops the current frame; Python looks for a matching handler in that frame, and if there is none the frame is discarded and the search continues in the caller, frame by frame (stack unwinding)
- On the way up every
finallyblock and everywithblock exit (__exit__) runs;exceptclauses that do not match are skipped - The traceback is built as the exception passes each frame: a linked list of traceback objects (
tb_frame,tb_lineno,tb_next) one.__traceback__, outermost frame first - Propagation stops at a matching
except, at a context manager whose__exit__returns true (for examplecontextlib.suppress), or at the top level, where the interpreter prints the traceback and exits with status 1 - Bare
raisekeeps the traceback unchanged whileraise eadds the current line; raising inside a handler replaces the exception (keeping it as__context__) - Exceptions do not cross thread boundaries on their own: an uncaught error in a thread goes to
threading.excepthook;Future.result()re-raises it in the caller
Clarifying questions expected:
- Is any of this running in a thread, a generator or an async task? (Each changes who receives the exception.)
Code / implementation expected: Optional โ a chain of three or four functions with finally blocks and one handler at the top, printing the order of events.