A try block can fail in more than one way. Python lets you catch each type separately, or group several together.

Multiple except blocks

You can write more than one except clause. Python checks them top to bottom and runs the first one that matches:

text: str = input("Enter a number: ")

try:
    number: int = int(text)
    result: float = 100 / number
    print(result)
except ValueError:
    print("That wasn't a number.")
except ZeroDivisionError:
    print("Can't divide by zero.")

If the user types "hello", ValueError runs. If they type "0", ZeroDivisionError runs.

Catching several types in one block

If you want the same handler for several exception types, list them in a tuple:

try:
    do_something_risky()
except (ValueError, TypeError, KeyError) as e:
    print(f"Bad input: {e}")

This is more concise when the response is the same.

Order matters — most specific first

Exception classes form a hierarchyLookupError is the parent of both KeyError and IndexError, and Exception is the parent of almost everything. A handler for a parent class catches all its children.

try:
    {}["missing"]
except LookupError:        # catches KeyError, IndexError, etc.
    print("missing item")
except KeyError:           # this can never run — LookupError caught it first
    print("missing key")

Always put the specific exceptions first, the general ones last:

try:
    risky()
except KeyError:
    print("missing key")
except LookupError:
    print("other lookup problem")
except Exception:
    print("anything else")

else — run when no exception happens

A try block can have an optional else clause that runs only if the try didn’t raise:

try:
    n: int = int(input("Number: "))
except ValueError:
    print("Bad input")
else:
    print(f"You entered: {n}")

What goes in else is code that you only want to run on success, but don’t want wrapped in the try (because you don’t want errors there to be caught by the same handler).

Most code skips else — putting the success code at the end of the try block is usually clearer.

A small example with multiple branches

A program that reads a number from the user and reports something about it:

text: str = input("Number: ")

try:
    n: int = int(text)
    reciprocal: float = 1 / n
except ValueError:
    print("Not a valid integer.")
except ZeroDivisionError:
    print("Can't take the reciprocal of zero.")
else:
    print(f"{n}{reciprocal:.4f}")

Each error gets a specific message. The success case is in the else.

Re-raising

Sometimes you want to log an error or do partial cleanup, then let it keep propagating. Use a bare raise:

try:
    risky_thing()
except ValueError:
    print("logging the error...")
    raise        # re-raises the same exception

This pattern is common in libraries — the library knows what failed, logs it, but doesn’t try to recover. Recovery is the caller’s job.

Raising a different exception

To translate an exception into a different one (often more domain-specific):

class ConfigError(Exception):
    pass


def load_port(text: str) -> int:
    try:
        return int(text)
    except ValueError as e:
        raise ConfigError(f"Port must be a number, got {text!r}") from e

The from e part attaches the original error as the cause. The traceback will show both — your caller sees the high-level ConfigError, but the underlying ValueError isn’t lost.

We’ll cover custom exception classes in a couple of lessons.

What’s next

You can catch many exceptions cleanly. Next, finally — code that always runs, exception or no exception.

Toggle theme (T)