Back to Blog
Python

Python Multiple Except Blocks: Ordering and Syntax

python multiple except blocks: Learn how to structure multiple except blocks in Python, order them correctly, group exception types, and preserve tracebacks for reliab...

Exception HandlingPython SyntaxError Handlingtry-exceptException Groups
Diagram showing multiple except blocks catching different exception types in Python code.

When you write python multiple except blocks, you're deciding how your program responds to different failure modes. The order and structure of these blocks determine which handler runs and what information is preserved. A common mistake is assuming that the interpreter picks the most specific handler automatically. In reality, Python evaluates except clauses from top to bottom and executes the first one that matches the raised exception. This behavior has direct consequences for how you organize your code.

Basic Syntax for Multiple Except Blocks

The simplest form of multiple exception handling is a try statement followed by several except blocks, each targeting a distinct exception type:

try: result = risky_operation() except ValueError: handle_value_error() except KeyError: handle_key_error() except TypeError: handle_type_error()

Each except block runs only if the raised exception is an instance of the specified type (or a subclass of it). The code inside the matching block executes, and then the interpreter skips the remaining except clauses and continues after the entire try statement. If no except matches, the exception propagates up the call stack.

This structure is useful when different exception types require genuinely different recovery logic. For example, a ValueError might indicate invalid input that should be reported to the user, while a KeyError might mean a missing configuration entry that should trigger a fallback default.

Why Exception Order Matters

Because Python checks except clauses in the order they appear, you must place more specific exception types before more general ones. Consider this example:

try: data = load_data() except Exception: handle_generic() except ValueError: handle_value_error()

Here, ValueError is a subclass of Exception. Any ValueError raised inside load_data() will be caught by the first except Exception block, and the except ValueError block will never run. The interpreter never reaches it because the first clause already matched.

This is not a syntax error; it is a logical flaw that silently changes behavior. To fix it, reverse the order:

try: data = load_data() except ValueError: handle_value_error() except Exception: handle_generic()

Now a ValueError is handled specifically, and any other Exception falls back to the generic handler. The same principle applies to any exception hierarchy. If you are catching both OSError and FileNotFoundError, the latter must come first because it is a subclass of the former.

Catching Multiple Exception Types in One Block

When several exceptions require the same handling logic, you can combine them into a single except clause using a tuple:

try: result = parse_user_input() except (ValueError, TypeError): log_invalid_input() result = None

This is equivalent to writing two separate except blocks with identical bodies, but it avoids code duplication. The tuple must contain exception classes, not instances. The handler runs if the raised exception is an instance of any of the listed types.

Grouping exceptions this way is particularly useful when the recovery action is identical. For example, a network operation might raise TimeoutError or ConnectionError, and both should trigger a retry with a backoff. Using a single block keeps the retry logic in one place and makes the intent explicit.

However, avoid grouping exceptions that require different responses. If one type should be logged and another should be silently ignored, separate blocks are clearer and safer.

Preserving the Original Traceback with raise from

Sometimes you need to catch an exception, perform cleanup, and then re-raise it or raise a new exception that wraps the original. Python provides the raise ... from syntax to chain exceptions:

try: process_file(path) except OSError as exc: log_error(exc) raise RuntimeError("Failed to process file") from exc

The from exc clause attaches the original exception as the __cause__ of the new one. When the new exception propagates, the traceback shows both the new error and the original cause. This is invaluable for debugging because it preserves the full context of the failure.

If you want to re-raise the exact same exception after handling it, use a bare raise inside the except block:

try: value = int(raw) except ValueError: log_warning("Invalid integer") raise

The bare raise re-raises the current exception without modifying its traceback. It does not create a new exception, so the original traceback remains intact. This pattern is useful when you want to log or perform cleanup but still let the caller handle the error.

Exception Groups and Python 3.11+

Python 3.11 introduced ExceptionGroup and the except* syntax for handling multiple unrelated exceptions raised simultaneously. This is a more advanced feature, but it is relevant when you work with concurrent tasks or complex validation that accumulates errors.

try: run_tasks() except* ValueError as eg: handle_value_errors(eg.exceptions) except* TypeError as eg: handle_type_errors(eg.exceptions)

Each except* block receives an ExceptionGroup containing only the exceptions that match its type. The remaining exceptions are re-raised as a new group. This allows you to handle different categories of errors independently while preserving the grouping.

Note that except* is only available in Python 3.11 and later. If you target older versions, you cannot use this syntax. For most applications, the traditional multiple except blocks are sufficient. Exception groups shine in scenarios like parallel processing, where several operations fail for different reasons and you want to report all of them rather than stopping at the first.

Performance and Maintainability Considerations

Catching exceptions in Python is not zero-cost, but the overhead is dominated by the exception raising mechanism itself, not by the number of except clauses. When an exception is raised, the interpreter unwinds the stack and checks each handler. The cost of checking a few extra except clauses is negligible compared to the exception creation and stack unwinding. Therefore, you should not sacrifice clarity for micro-optimizations in exception handling.

From a maintainability perspective, the key is to keep the except blocks focused. A single except clause that catches Exception and then uses isinstance checks to branch internally is usually worse than multiple explicit blocks because it obscures the control flow. Similarly, a bare except: that catches BaseException (including KeyboardInterrupt and SystemExit) is almost always a mistake because it prevents clean shutdown and can mask critical errors.

When you have many exception types, consider whether they share a common base class that you can catch once. For example, if you are working with a library that raises requests.RequestException for all network errors, you might only need one handler for that base class. This reduces duplication and makes the code easier to update when the library adds new exception types.

Another maintainability concern is the placement of except blocks. Keep them close to the operation that can fail, and avoid wrapping large sections of code in a single try block. A narrow try block makes it clear which exceptions can be raised and reduces the risk of accidentally catching unrelated errors.

Common Mistakes and How to Avoid Them

One frequent mistake is catching too broad an exception type. For example, except Exception catches every built-in exception except BaseException subclasses like KeyboardInterrupt and SystemExit. This can hide programming errors such as AttributeError or NameError, making debugging difficult. Instead, catch the specific exceptions you expect and let unexpected ones propagate.

Another mistake is using a bare except: clause. This catches BaseException, which includes KeyboardInterrupt and SystemExit. If your code is interrupted by a user pressing Ctrl+C, a bare except: will swallow that signal and continue running, which is rarely desired. Always specify an exception type, or at least except Exception if you must catch all normal errors.

Ordering errors are also common. As discussed earlier, placing a parent class before a subclass makes the subclass handler unreachable. This is a silent bug because Python does not warn you about it. To avoid this, list exceptions from most specific to least specific. If you have a hierarchy that you are unsure about, check the MRO (method resolution order) or consult the documentation.

Finally, avoid empty except blocks. An except block that does nothing silently swallows the error, leaving the program in an unknown state. If you truly want to ignore an exception, at least log it or add a comment explaining why it is safe to ignore. Better yet, restructure the code so that the exception is not raised in the first place, using checks or context managers where appropriate.

When to Refactor Multiple Except Blocks

If you find yourself writing many except blocks with similar logic, consider refactoring. One approach is to define a custom exception hierarchy that groups related errors under a common base class. Then you can catch the base class in a single handler and still have the option to catch specific subclasses elsewhere.

Another approach is to use a mapping from exception types to handler functions. This can be useful when the number of exception types is large and the handling logic is uniform:

handlers = { ValueError: handle_value_error, KeyError: handle_key_error, TypeError: handle_type_error, } try: result = operation() except Exception as exc: handler = handlers.get(type(exc)) if handler: handler(exc) else: raise

This pattern moves the branching out of the try statement and into a data structure, which can be easier to maintain if the set of exceptions changes frequently. However, it loses the syntactic clarity of multiple except blocks and makes the control flow less obvious. Use it only when the mapping is genuinely dynamic or when you need to configure handlers at runtime.

In most cases, a small number of explicit except blocks is the most readable and maintainable solution. The goal is to make the error-handling path as explicit as the happy path, so that a future reader can see exactly what can go wrong and how the program responds.

python multiple except blocks: Practical Usage and Code Exam | RYUSLOG DEV