AlgoMaster Logo

try-except

High Priority26 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

This lesson is about the four-part statement Python gives you to deal with exceptions: try, except, else, and finally. Each clause has one job, and the order they fire in is fixed. By the end you'll know which clause to use for what, how to catch one exception type or many, how to grab the exception object itself, and which patterns quietly hide bugs.

The Four Clauses at a Glance

A full exception-handling block in Python can have up to four clauses. You write try once, then any combination of except, else, and finally. Each one runs at a specific point in the flow, and each one answers a different question.

ClauseWhen it runsWhat it's for
tryFirst, alwaysThe risky operation you want to guard
exceptOnly if a matching exception is raised in tryRecovering from a specific failure
elseOnly if try finished with no exceptionCode that should run only on the happy path
finallyAlways, exception or notCleanup that has to happen no matter what

The minimum is try plus at least one except or finally. A try on its own is a syntax error, and so is else without an except. Beyond that, you mix and match. The most common shapes are try/except, try/except/finally, and the full try/except/else/finally when you want all four jobs handled cleanly.

Here's a small example that uses all four clauses on a product price lookup:

Notice that "Lookup attempt complete" prints on both runs, but "Found 'mug'" only prints on the successful one, and "'phone' not in catalog" only prints on the failing one. That's the whole pattern in miniature. The rest of the lesson is just details on each clause and the rules that connect them.

try and except: The Basics

A try block marks the code you want to guard. An except block says "if this specific kind of exception happens in the try, here's how to handle it." If the try runs cleanly, the except block is skipped. If an exception is raised and it matches the type listed on the except, control jumps to the handler.

The dict lookup raises KeyError. Python checks the except clauses in order, finds one that names KeyError, and runs that handler. The program keeps going from after the whole try/except block.

Catching an exception also catches its subclasses. KeyError is itself a subclass of LookupError, and LookupError is a subclass of Exception. Any of these three would catch the same dict-lookup error:

The rule to remember is: except SomeException catches SomeException and every class that inherits from it.

Catching Specific Exceptions

Catching a specific exception type is the version you should reach for most of the time. It says "I know this exact failure can happen here, and here's what to do about it." Anything else falls through and becomes a real crash, which is usually what you want.

The function only catches ValueError, which is what float() raises for strings that don't look like numbers. If a caller passed something weirder, like a list, float([]) would raise TypeError. That wouldn't match the except clause, so it would propagate up and crash. That's fine. The function never claimed to handle TypeError; it claimed to handle "this string doesn't look like a number," and that's exactly the failure it catches.

The opposite approach, catching everything, looks like this:

This catches ValueError, TypeError, MemoryError, KeyboardInterrupt, and every other exception in Python. It also catches typos in your own code. If you misspell a variable name inside the try, that's a NameError, and this handler would silently turn it into None. Bare except is almost always wrong.

The takeaway for now: catch the smallest exception type that covers the failure you actually want to handle. Everything else should be allowed to crash so you can find out about it.

Catching Multiple Exceptions

Sometimes one piece of risky code can fail in several distinct ways, and you want the same recovery for each. Python gives you two ways to handle that: list the types in a tuple after one except, or write multiple except blocks with different handlers.

The tuple form groups types that share a handler:

Each call fails in a different way. The empty list raises IndexError. The dict without a "name" key raises KeyError. The string can be indexed, but "not a cart"[0] is "n", and "n"["name"] raises TypeError because strings don't index by string. All three failures land in the same handler.

Use the tuple form when the response is the same for every type in the group. If the response differs, write separate except blocks:

Two different failures, two different reactions. The first call works normally and the print line never fires. The second hits KeyError and adds the product. The third hits TypeError because you can't do "not a cart"["pen"] += 5, and the second handler reports it.

Order Matters: Specific Before General

When you stack multiple except blocks, Python checks them in source order and takes the first one that matches. Because catching an exception also catches its subclasses, putting a general handler before a specific one means the specific handler can never fire.

Here's the trap:

KeyError is a subclass of Exception, so the first except matches and Python never even looks at the second. The second handler is dead code. CPython doesn't warn you about this by default, although some linters will.

Swap the order and both handlers become reachable:

Now KeyError matches the first clause and runs the specific handler. A different exception type, say a TypeError, would skip the first clause and fall through to the second.

The rule is simple: most specific first, most general last. If you're catching three or four related exception types, write them in the order from narrowest to widest. A general except Exception: at the bottom is occasionally useful as a last-resort net, but most of the time you should list the specific types you actually expect and let anything else crash.

The diagram traces what Python does at runtime. After the try block, if no exception was raised, the whole try/except is done and control moves on. If something was raised, Python walks the except clauses in source order. The first match wins. If nothing matches, the exception propagates upward and either gets caught higher up or eventually crashes the program.

Capturing the Exception with as

Catching an exception by type tells you what went wrong, but often you also want to know the details: which key was missing, which value failed to parse, what the original message said. The as clause binds the exception object to a name so you can use it inside the handler.

Printing the exception object gives you the message Python would have shown in the traceback. The as e part isn't required. You can write except ValueError: with no name if you don't need the details. But once you do need them, as is how you get them.

The exception object carries more than just its message. It has an args attribute (the arguments it was constructed with) and a __traceback__ attribute (the call stack at the time it was raised). For most logging, str(e) or just print(e) is enough.

KeyError is one of the few exception types where str(e) adds quotes around the value, because that's how KeyError.__str__ is implemented. The raw missing key sits in e.args[0], which is sometimes easier to use programmatically.

One gotcha worth knowing: the name you bind with as is only visible inside the except block. After the block ends, Python deletes the name. This is intentional, because the exception holds a reference to the traceback, which holds references to every local variable in the frames where it was raised. Letting that linger would keep large amounts of memory alive.

captured survives because you bound it to a separate name. e is gone the moment the except block ends.

The else Clause: Code That Runs Only on Success

The else clause is the one most newcomers skip. It runs only if the try block finished without raising an exception, and it runs after the try but before any finally. The point is to separate "this can fail" code from "this only makes sense if the above succeeded" code.

Without else, you tend to write this:

That works, but now the print line lives inside the try. If print itself raised an exception for some reason (it can, if the stream is closed), the except KeyError: wouldn't catch it, which is fine, but the print would still be "guarded" by a clause that wasn't meant to guard it. More importantly, you've blurred the line between "the operation we're guarding" and "what we do with the result."

With else, the two paths are cleanly separated:

The try block contains exactly one risky line. The except handles the failure. The else runs only when the lookup succeeded, and price is guaranteed to be bound when it runs.

The other reason else matters: it makes the scope of your except honest. If you put everything in the try, a bug five lines down might get swallowed by the same except that was only supposed to catch the lookup. Keeping the try short and pushing the rest into else prevents that.

If save_order_record itself raised a KeyError for unrelated reasons, the handler above wouldn't catch it. That's correct behavior. The try was about the price lookup, not about saving. else made that boundary explicit.

The finally Clause: Always Runs

The finally clause runs no matter what happened in the try. Exception or not, caught or uncaught, return statement or normal flow, the finally block fires before control leaves the try/except/else/finally structure. That makes it the natural home for cleanup: closing a file, releasing a lock, restoring some setting, logging that an attempt finished.

If file.read() raises, the finally still runs and closes the file, then the exception propagates. If file.read() succeeds, the finally still runs. Either way, the file isn't left open. In real Python code you'd use a with statement here, but finally is the lower-level mechanism that with is built on.

A practical e-commerce example: imagine a counter that tracks how many lookups your catalog has handled, regardless of whether they succeeded:

The counter goes up on every call, hit or miss. If the function had ten different return paths and three exception handlers, finally would still fire exactly once per call.

finally runs even when there's no matching except. If a try raises a type that no handler catches, Python runs the finally block first, then propagates the exception:

cleanup prints first, then the exception bubbles up to the top of the program.

When finally Eats Your Exception

finally always runs, and that "always" has a sharp edge. If the finally block returns a value, or raises its own exception, the original exception or return value from the try/except/else part disappears. This is a real gotcha. Code that should crash quietly succeeds, and you spend an afternoon wondering where your error went.

The classic example, a return inside finally:

The first call should return 5. The second should propagate the KeyError from the explicit raise. Neither happens. The return -1 in finally wins over both. Python evaluates the try/except to figure out the answer, then finally runs and replaces that answer with its own return.

The fix is to not return from finally. Use it for cleanup, not for results.

The same effect happens if finally raises:

The lookup succeeded, but the new exception from finally replaces the return value. If the try had raised its own exception, that one would be lost too, although Python at least chains them so the original shows in the traceback as the context.

Two rules to live by:

  • Never return from a finally block. If you need to change the return value based on whether the try succeeded, do it in else or after the whole block.
  • Don't raise new exceptions from finally unless you really mean to. Treat finally as a place where things should not fail.

Bare except, except Exception, and except BaseException

You can write except with no exception type at all. This is called a bare except and it catches literally everything that can be raised in Python.

A bare except catches KeyboardInterrupt (Ctrl+C), SystemExit (calls to sys.exit()), and MemoryError. None of these are usually things you want to swallow. If a user presses Ctrl+C to kill your program, they expect it to actually stop. A bare except can quietly turn that into "something went wrong" and keep running.

The standard recommendation is to write except Exception instead. Exception is the base class for almost every exception you actually want to catch (ValueError, KeyError, TypeError, custom application errors, and so on), but it does not include KeyboardInterrupt, SystemExit, or GeneratorExit.

Now Ctrl+C still stops the program, but every "ordinary" exception is caught.

PatternCatchesWhen to use
except SomeType:SomeType and its subclassesThe default. Catch what you can actually handle.
except Exception:Most exceptions, but not Ctrl+C or SystemExitA last-resort catch-all at a top-level boundary.
except BaseException:Everything, including Ctrl+C and SystemExitAlmost never. Usually a mistake.
except: (bare)Same as except BaseException:Almost never. Usually a mistake.

Even except Exception should be rare. Use it at the outer edge of a program, where you want to log unexpected failures and keep a long-running service alive, not in the middle of business logic where it would mask bugs.

That's a reasonable outer boundary. Inside run_checkout, you should catch specific exceptions, not all of them.

How Python Evaluates the Whole Block

When you stack all four clauses, the order Python uses is fixed. It's worth seeing it laid out once so the rest of the lesson clicks together.

Reading this top to bottom: the try runs first. If nothing is raised, else runs, then finally, then we move on. If something is raised, Python checks the except clauses. A match runs the handler, then finally, then we move on. No match runs finally and then propagates the exception up the call stack. finally is always on the path; else is only on the no-exception path; the handler is only on the matched-exception path.

Here's a script that demonstrates each branch by printing what runs:

Three scenarios, three paths. Clean run hits try, else, finally. Caught exception hits try, except, finally (no else, because an exception fired). Uncaught exception hits try, finally, and then propagates to the outer handler. The else branch never runs on a path where the try raised. That's the single rule that ties all this together.

Exception Groups: except* (Python 3.11+)

Python 3.11 added a new flavor of exception called an exception group, plus a new except* syntax for handling them. The motivating use case is concurrent code: when you run several tasks in parallel and more than one of them fails, you want to receive all the failures together rather than just the first one. PEP 654 has the full design rationale.

A regular except clause can't unpack a group. It either catches the whole group or none of it. except* is the version that knows how to split a group, handle the parts that match a given type, and let the rest re-raise.

ExceptionGroup collects related exceptions into one object. except* ValueError catches every ValueError inside the group (no matter how deeply nested), bundles them into a sub-group, and runs the handler. The TypeError in the same group falls through to the second except* clause. If neither clause caught everything, the remaining exceptions would propagate as a smaller group.

This syntax is most useful inside asyncio.TaskGroup and similar APIs, where many tasks run together and any subset of them can fail. For straight-line code, plain try/except is still the right tool. The full rules are in PEP 654.

Putting It All Together

A realistic e-commerce example: a function that loads a single product from a price file, parses a quantity, and reports the line total. It uses every clause and shows them working in concert.

If the file is missing, the first try/except catches the FileNotFoundError and returns early. The second block never runs. If the file opens but a line is malformed, the second try raises ValueError from the split or the float call, the except handles it, and the finally still closes the file. On a clean run, else prints the count and returns the dict, and again finally closes the file. Three different paths, one set of clauses, and the file is closed every time.

This is the shape exception handling takes once you're comfortable with the four clauses: small, focused try blocks; specific except types; an else for the "everything worked" path; and a finally that handles cleanup without caring about which path you took.

Quiz

try-except Quiz

10 quizzes