Current PyPy: runtime linked list of FrameBlock objects (ExceptBlock, FinallyBlock, SysExcInfoRestorer) anchored at frame.lastblock. The compiler emits SETUP_FINALLY/SETUP_EXCEPT/POP_BLOCK to manage this list. On exception, unrollstack() walks the list to find the handler.
CPython 3.11: no block stack at all. The exception table co_exceptiontable maps bytecode offset ranges to (handler_offset, stack_depth, lasti_flag). Only consulted when an exception actually fires — zero overhead on the happy path.