What raise actually does
In plain words Picture an office where every call opens a desk on top of the last. Someone at the top desk hits a problem they can't solve, writes it on a card, clears their desk and hands the card down. The next person either deals with it or clears their own desk and passes it on. If it reaches the front door unanswered, the card is pinned to the wall (that's the traceback) and the office closes for the day.
raise X first evaluates X, which is normally a constructor call, so an instance of an exception class appears on the heap. The interpreter then attaches a traceback entry for the current frame and looks for a try block covering the current line. If there's none, the frame is popped, locals and all, just as on return, and the search continues in the caller at the line that made the call, which gets a traceback entry of its own.
Only a matching except stops the walk. If nothing matches before the
module level, the traceback is printed to stderr and the process exits
with status 1.
An ordinary instance
ValueError("bad data") is a Session 04 constructor call: a
__class__ link, an args tuple, and room for a traceback.
Frames are popped
Each frame without a handler is cleared and adds one entry to
__traceback__. The locals die with the frame. The exception object doesn't.
A handler, or the wall
The first frame whose except matches keeps its desk and runs the handler. Otherwise the traceback is printed, the exit status is 1, and nothing else runs.
__traceback__ row from the memory panel, one frame per entry.
try: 1 / 0 then except ZeroDivisionError as e: tb = e.__traceback__.
Now inspect it: tb.tb_frame.f_code.co_name and tb.tb_lineno
tell you where, and tb.tb_next walks toward the raise site. It's the same chain the panel draws, and the same one traceback.print_exc() prints.
try, except, else, finally
In plain words
A try block is a desk with a note on it: "if a card arrives while I'm on one of these lines, check it against this list". The list is the except clauses, read top to bottom. The first one that fits handles it, and the rest are ignored.
The match is isinstance(exc, ExceptionClass), so a clause catches the named class and every subclass of it. except LookupError catches a KeyError, and except Exception catches nearly everything you'll ever raise yourself. else runs only if the try body finished without raising, and finally runs no matter what. The next section is about that.
except.
always
isinstance match runs. If nothing matches, the frame is cleared and the climb continues.
first match
else block isn't swallowed by the except above it.
no error
return.
always
as err is a normal name that gets deleted for youexcept ... as err block Python executes
del err. Reading err afterwards gives
NameError. The reason is a cycle: the exception references its
traceback, the traceback references the frames it walked through, and the frame you're in would reference err. That loop would keep every local in every one of those frames alive until the cyclic collector got round to it. If you need the exception later, bind it to another name inside the block.
The family tree decides what a clause catches
Exceptions are classes, and except matches by inheritance. A handful of parents cover most of what you'll meet:
| You raise or meet | Its parents | So this clause catches it |
|---|---|---|
KeyError, IndexError |
LookupError → Exception |
except LookupError |
ZeroDivisionError, OverflowError |
ArithmeticError → Exception |
except ArithmeticError |
FileNotFoundError, PermissionError |
OSError → Exception |
except OSError |
ValueError, TypeError, StopIteration |
Exception |
except Exception |
KeyboardInterrupt, SystemExit |
BaseException only |
a bare except:, and almost never on purpose |
except: catches Ctrl-CKeyboardInterrupt and SystemExit deliberately sit outside
Exception, so that except Exception: lets the user quit
and lets sys.exit() work. A bare except: catches them
too, which is how a script ends up impossible to interrupt. Name the class you mean. If you really do mean "anything my code could raise", write except Exception.
KeyError.__mro__ prints the whole chain up to object.
Then try: {}["x"] with except LookupError as e: print(type(e).__name__). It prints KeyError, because the clause matched a subclass.
Finally, after any except ... as e block, type e: NameError.
finally runs before the frame is left, whatever the reason
In plain words
"Lock the door on your way out" applies whether you leave because you finished, because you were called away, or because you're running from a fire.
finally is that rule. Demo 3 shows the version people find hardest to
believe: a return inside the try still runs the
finally block first.
When control leaves a try that has a finally, whether by falling off the end, by return, break or continue, or by an exception, the interpreter parks the pending result (the return value, or the exception in flight), runs the finally block, and then resumes whatever was pending. A return inside finally replaces the pending result, which throws away an exception that was on its way out.
The block runs on every exit
Files get closed, locks get released, temporary state gets restored, even when the line that failed is one you didn't think could fail.
It doesn't cancel the exception
After the block, the parked exception carries on climbing. The only thing that stops it is a return, break or continue inside finally, and Python 3.14 warns about all three
(PEP 765)
because they almost always hide a bug.
Define def f(): try: return "from try" finally: print("cleanup") on
separate lines and call it. cleanup prints before the value
comes back. Then change the finally to return "from finally"
and see which string wins.
with is try/finally with the cleanup moved into the object
In plain words
Instead of every caller having to remember to lock the door, the door locks itself. An object that knows how to set itself up and tear itself down is a context manager, and with is the two-line way of saying "set up, do this, tear down, no matter what".
with EXPR as name: evaluates EXPR to an object, looks up __enter__ and __exit__ on its class, calls __enter__() and binds its return value to name, runs the body, and then calls __exit__(exc_type, exc, tb), with three Nones if the body finished, or with the exception and its traceback if it didn't. If __exit__ returns a true value the exception is suppressed. Otherwise it continues as if the with had never been there.
What you write
with open(path) as f: then a body that uses f. Two lines,
no cleanup in sight. Demo 4 is the success path; demo 5 is the body raising.
What runs (roughly)
mgr = open(path); f = type(mgr).__enter__(mgr);
a try around the body; on an exception,
__exit__(type, exc, tb) and re-raise unless it returned true;
on success, __exit__(None, None, None).
Managers you already use
| Expression | as gives you |
__exit__ does |
|---|---|---|
open(path) |
the file object itself | closes it, which is why with open(...) is the idiom rather than f = open(...) |
threading.Lock() |
True, so nobody writes as |
releases the lock, which is why Session 07 said to always take it with with |
tempfile.TemporaryDirectory() |
the path, as a string | deletes the whole directory |
sqlite3.connect(...) |
the connection | commits on success, rolls back on an exception, and does not close the connection |
contextlib.suppress(FileNotFoundError) |
None |
returns True for the named classes, swallowing them |
yield@contextlib.contextmanager turns a generator function into a context
manager: everything before the yield is __enter__, the
yielded value is what as binds, and everything after it is
__exit__. If the body raises, the exception is thrown into the paused generator at the yield, so wrap the yield in try/finally if the cleanup must happen either way. It's the same suspended frame as in Session 05, with a different caller pulling the strings.
with 42: pass gives TypeError: 'int' object does not support the context manager protocol, raised before the body ever runs. Then
with open("x.txt", "w") as f: pass followed by f.closed: the
name survived the block and reports True.
One error causes another
In plain words Sometimes the person who receives the card can't fix the problem, but can describe it better. "The file is corrupt" is more useful to the caller than "invalid literal for int()". Python lets you raise the better card while keeping the original stapled to it, and the traceback shows both.
Any exception raised while another is being handled gets __context__ set to the one being handled, automatically. raise New from old also sets __cause__ and __suppress_context__ = True. The printer uses those attributes to choose its sentence, and from None hides the first exception from the output while leaving it reachable through __context__.
Inside an except block |
Attributes on the new exception | The traceback says |
|---|---|---|
raise RuntimeError("bad input") |
__context__ → the original |
During handling of the above exception, another exception occurred: |
raise RuntimeError("bad input") from err |
__cause__ and __context__ → the original |
The above exception was the direct cause of the following exception: |
raise RuntimeError("bad input") from None |
__context__ → the original, but suppressed |
only the new exception is printed |
raise (bare) |
the same object, unchanged | the original traceback, continued from here |
raise, not raise err, to re-raiseraise err adds a fresh traceback
entry for the line it is on, so the same function appears twice in the printed traceback, once for the original failure and once for the re-raise. A bare raise continues the traceback where it was.
A stack of frames with one drawn red, an exception instance on the heap whose
__traceback__ lists every frame it has cleared, a handler frame that keeps its desk, and, for chaining, a second exception whose __cause__ arrow points back at the first. If you can draw that, every
traceback you meet is a picture you have already seen.
Six ways this goes wrong
Catching too much
except Exception: pass turns every bug in the block (a typo, a None where a list should be, a missing key) into silence. Catch
the class you expect, at the place you can actually recover. If you only want to
log and move on, log the traceback (logging.exception(...)) so the bug
still leaves a trail.
A bare except:
It catches KeyboardInterrupt and SystemExit as well, so
Ctrl-C stops working and sys.exit() gets swallowed. The widest net
that still lets the program be stopped is except Exception.
Using err after the block
except ValueError as err: then print(err) two lines later gives NameError: name 'err' is not defined. Python deleted it to break the cycle between the frame and the traceback. Bind it to another name inside the block if you
need it afterwards.
return inside finally
It overrides the pending return value and discards any exception in
flight. The failure isn't an error you see. It's an error you never see. Python 3.14 emits a SyntaxWarning for it. Put the return after the try statement instead.
Opening a file without with
data = open(path).read() works on CPython because the file object is freed, and therefore closed, the moment its reference count hits zero.
Keep the object alive by accident, or run under PyPy, and the handle stays open.
with open(path) as f: closes it on a line you can point at.
Several classes without the parentheses
except ValueError, TypeError: is a SyntaxError in every
Python up to 3.13 (multiple exception types must be parenthesized). Write
the tuple: except (ValueError, TypeError) as err:. Python 3.14 finally allows the bare form when there's no as, but the tuple works everywhere.
try goes, and it's usually further up than the line that fails. What must happen on the way out, even if it fails? That's a finally, or better, a with.