Real code rarely fails in just one way. A function that reads a product from a catalog can hit a missing key, a malformed number, a closed file, and a network blip, sometimes all in the same week. Python lets you handle each of these on its own terms. This lesson covers how to stack multiple except clauses, how to group several types under one handler, why the order of clauses matters, when (and why not) to use a broad catch-all, how to log what was caught, how to re-raise after handling, and how ExceptionGroup plus except* deal with parallel failures.
except Clauses on One tryA single try block can be followed by as many except clauses as needed. Python checks them top to bottom and runs the first one whose type matches the raised exception. Anything that doesn't match propagates out as if no try were there.
Three calls, three different paths through the same block. The first one finds the product, parses the quantity, and returns the line total. The second one hits the dict lookup first, so the KeyError handler runs and the rest of the try body never executes. The third one passes the lookup but fails on int("two"), which raises ValueError and lands in the second handler.
Each except handles exactly one kind of failure. Splitting them like this keeps the function readable: a reader can scan the handlers and see which problems the function knows how to recover from, and what it does in each case. A single combined handler would force the reader to dig through conditional logic to figure out the same thing.
Adding more except clauses costs nothing on the happy path. Python only walks the list when something is actually raised, and the search is fast.
If two or more exception types should be handled the same way, write them as a tuple inside one except. This avoids copy-pasted handler bodies.
Four different inputs, four different outcomes. The full review object returns its text. The empty list raises IndexError. The review without a "text" key raises KeyError. The None raises TypeError because NoneType is not subscriptable. All three failures fall into the same except and produce the same fallback string.
The parentheses around the types are required. except IndexError, KeyError: was old Python 2 syntax for except IndexError as KeyError:, and it doesn't work in Python 3 at all. The modern form is always except (Type1, Type2): with a tuple.
Use the tuple form when the response is the same for every type. If the handler body contains an isinstance check on the exception to branch on its type, separate clauses are a better fit.
When multiple except clauses are stacked, Python walks them in source order and stops at the first match. Because catching a class also catches its subclasses, putting a broad parent class above a narrower child class makes the child handler unreachable.
The relevant corner of the exception hierarchy looks like this:
KeyError and IndexError are both subclasses of LookupError, which is a subclass of Exception. So except LookupError: catches both, and except Exception: catches every class in this diagram.
The trap that follows from this:
The second handler is dead code. KeyError inherits from Exception, so the broad clause matches first and Python never checks the narrower one. CPython doesn't raise an error or a warning for this. Static checkers like ruff and pylint flag it, but only when those tools are run.
Flipping the order makes both handlers reachable:
Now KeyError matches the first clause. A different exception type, say a TypeError from a bad operation later in the try, would skip the first clause and fall through to the second.
The rule is short: most specific first, most general last. When stacking three or four handlers, sort them from narrowest type to widest. A final except Exception: safety net belongs at the bottom.
A more realistic version with three layers, each stricter than the one below it:
The first call updates an existing product cleanly. The second misses on the lookup, lands in KeyError, and adds the new product. The third hits TypeError because strings don't support item assignment. If any fourth scenario raised something not listed (say a MemoryError), the final except Exception would catch it as a safety net. That last clause isn't strictly necessary; one alternative is to leave unexpected errors uncaught so they surface as real bugs. Either way, the ordering rule still holds.
except Exception and Bare exceptTwo patterns look similar but behave very differently. One is a reasonable last-resort net. The other is almost always a bug.
except Exception: catches every exception class that inherits from Exception, which covers the vast majority of errors in normal code: ValueError, KeyError, TypeError, RuntimeError, custom application exceptions, and so on. It does not catch KeyboardInterrupt (raised when the user presses Ctrl+C), SystemExit (raised by sys.exit()), or GeneratorExit (used internally for generator cleanup). Those three inherit from BaseException directly, on a separate branch of the tree, and they're meant to flow through normal handlers without being absorbed.
This is the pattern for a top-level boundary: the outer edge of a request handler, a worker loop, or a script's main function. Unexpected failures should be logged and possibly reported, not crash the entire long-running process. Inside the business logic itself, catch specific exceptions instead, because a broad except Exception: in the middle of the code will swallow real bugs and make them invisible.
A bare `except` (just the word except with no class) is the same as except BaseException:. It catches everything, including Ctrl+C and sys.exit().
If a user presses Ctrl+C while the program is inside this try, the except absorbs it and the program keeps running. That defeats the whole point of Ctrl+C. A bare except also catches SystemExit, so sys.exit() calls inside the protected block do nothing. Both behaviors are almost always wrong.
| Pattern | Catches | When to use |
|---|---|---|
except SomeType: | SomeType and its subclasses | The default. Catch what you can actually handle. |
except (TypeA, TypeB): | Either listed type and their subclasses | When several types share one handler |
except Exception: | All ordinary exceptions, but not Ctrl+C or SystemExit | A last-resort net at a top-level boundary |
except BaseException: | Everything, including Ctrl+C and SystemExit | Almost never; usually a mistake |
except: (bare) | Same as except BaseException: | Almost never; usually a mistake |
When a bare except shows up in existing code, the usual fix is to replace it with except Exception so the control-flow signals (Ctrl+C, sys.exit) still work. Naming the specific exception types expected is better still.
Catching an exception silently is almost as bad as not catching it. If a function returns a default value on every failure, there is no way to know how often the failure is happening, what the actual problems look like, or whether the fallback is masking a real bug.
The as clause binds the exception to a name so its details are readable. The simplest useful pattern is to print or log it before the handler does its recovery:
The function still returns 0.0 on a miss, but each miss leaves a trace. The logs show which products are missing and how often, which helps decide whether the catalog needs to be updated or the fallback is hiding a real catalog bug.
For tougher failures, the logging module has a method specifically for exception logging:
logging.exception adds the full traceback to the log entry, which is what debugging usually requires. It is the same as calling logging.error(msg, exc_info=True). Plain print(e) only shows the message, which is fine for an interactive script but loses information needed later.
A common mistake is logging just the type of the exception and dropping the message:
That tells you something failed but not what. Always include the message ({e}), and prefer logging.exception when you want the traceback too.
Sometimes the handler needs to do something (log it, increment a counter, clean up a partial state) but not actually recover from the failure. raise on its own with no argument re-raises whatever exception is currently being handled.
The handler logs and increments the counter, then raise sends the same KeyError up to whoever called fetch_price. The caller still has full control over how to respond, but now there's a record of every miss along the way. The original traceback is preserved exactly as it was, so anyone reading the eventual error report sees the real line where the lookup failed.
A bare raise is different from raising a new exception. Writing raise KeyError(product) instead creates a fresh exception object with a fresh traceback, and the original one becomes the cause. For "log and forward" handlers, bare raise is the right form.
The diagram shows what happens at runtime. fetch_price doesn't make the error disappear. It briefly catches it, runs its handler, and then raise puts the same exception back in flight. From the caller's point of view, the function looks like it raised KeyError directly, which is the desired behavior.
A useful variation is to filter inside the handler and re-raise selectively. Catch the exception, inspect its details, decide whether to recover, and re-raise if the conditions don't match:
Adding a positive quantity for a missing product is fine and gets handled inside. Removing one from a product that isn't in the cart isn't something the function knows what to do about, so it re-raises and lets the caller handle it. Inside the except block, the original exception is still in flight and raise with no argument refers to it. Once the except block exits normally (without re-raising), Python considers the exception fully handled.
ExceptionGroup and except* (Python 3.11+)The handlers covered so far deal with one exception at a time. That model breaks down for parallel work. If five tasks update five different products in a cart and three of them fail, the goal is to know about all three at once, not just whichever one happens to bubble up first.
Python 3.11 introduced exception groups (PEP 654) to solve this. An ExceptionGroup wraps a collection of exceptions and presents them as a single object that can be raised, caught, and inspected. The companion syntax except* knows how to split a group: it takes the exceptions that match its type, hands them to the handler, and lets the rest re-raise.
A small example that validates a cart and reports every problem at once instead of stopping at the first one:
validate_cart collects every problem it finds rather than raising on the first one. When it has at least one issue, it bundles them all into an ExceptionGroup and raises that. The caller writes two except* clauses, one per type. Each clause receives a sub-group containing only the exceptions of its type. The handler can iterate over group.exceptions to walk them one at a time.
Three properties of except*:
ExceptionGroup. A plain ValueError raised directly doesn't get caught by except* ValueError. Use it for the cases that specifically deal with grouped failures.except* clause sees a sub-group, not a single exception. Even if only one exception of that type was in the group, the handler still receives a one-element group with that exception inside.except* clauses propagate up as a smaller ExceptionGroup containing just the unmatched ones. Nothing gets dropped.The common place to meet ExceptionGroup is asyncio.TaskGroup, which uses this machinery to surface every task's failure together when concurrent tasks crash. For straight-line code, regular try/except is still the right choice; except* is well-suited specifically to cases where several independent things can fail at the same time.
A realistic example that ties most of this together: a function that processes a single line from a CSV-style order file, with handlers for the failures it actually expects, logging on every failure path, and a re-raise for truly unexpected problems.
Three of the five lines fail, each in a different way. The function catches the two types of failure it knows how to recover from (KeyError and ValueError), logs each one with enough context to diagnose later, and returns 0.0 so the surrounding sum keeps going. The third handler is the safety net: if something unexpected happens (a MemoryError, a RecursionError, a custom exception not covered above), the function logs the full traceback and re-raises so the caller can decide whether to crash. The total at the end is the price of the two successful lines and nothing else.
This is the shape granular error handling tends to take: specific handlers for known failure modes, generic logging that captures the actual error, and a deliberate decision at the bottom of the stack about whether to swallow, fall back, or propagate.
A few short rules that the rest of the lesson has been pointing at:
except Exception: for top-level boundaries; never use bare except:.logging.exception for tracebacks and include the message, not just the type.ExceptionGroup and except* for parallel failures.** They fit asyncio.TaskGroup and similar concurrent APIs; they are overkill for straight-line code.The thread connecting all of these: every except clause is a promise. It says "this function knows how to handle this specific failure here, in this specific way." Vague or overly broad handlers break that promise, and the result is code where errors hide, debugging is painful, and bugs survive longer than they should.
10 quizzes