Real code rarely has just one way to fail. python multiple exceptions handling covers the situations where a single try block might raise several different kinds of errors — and, for a genuinely different problem, where several exceptions need to be raised and handled together, not just one at a time. This article covers grouping exceptions in a tuple, stacking separate except blocks, catching broadly (safely), and the ExceptionGroup/except* syntax introduced in Python 3.11.
Introduction: Why One Except Block Isn't Always Enough
Consider a function that parses user input and does some math with it — it could plausibly raise a ValueError (bad input format), a ZeroDivisionError (dividing by zero), or a FileNotFoundError (if it's also reading from a file). Different failure modes, potentially needing different — or sometimes identical — handling.
Three approaches, previewed
This article covers three distinct strategies: grouping exceptions in a tuple (one block, several exception types, identical handling), multiple separate except blocks (different handling per exception type), and — for a genuinely different scenario — ExceptionGroup/except*, for when several unrelated exceptions occur simultaneously, not just one at a time.
A key limitation to set up front
Traditional try/except only ever catches the first exception raised inside the try block — the moment an exception is raised, execution jumps immediately to the matching except, and nothing else in the try block runs at all. This works fine for the common case of "one operation, one possible failure at a time." But it genuinely can't handle a scenario where multiple, unrelated exceptions occur simultaneously — say, several independent tasks running concurrently, each failing with its own separate exception. Section 5 covers exactly this case.
Catching Multiple Exceptions in One Block
Tuple syntax
try:
value = int(input("Enter a number: "))
except (ValueError, TypeError) as e:
print(f"Invalid input: {e}")Listing several exception types inside parentheses, separated by commas, lets a single except block catch any of them, with identical handling applied regardless of which specific one actually occurred.
When this makes sense
Tuple grouping is the right choice specifically when the recovery logic is genuinely the same no matter which of the grouped exceptions was actually raised. If your response to a ValueError and a TypeError would be identical either way, there's no real benefit to separating them into two blocks — it would just be needless duplication.
A practical example
def parse_and_double(value):
try:
number = int(value)
return number * 2
except (ValueError, TypeError) as e:
print(f"Could not process '{value}': {e}")
return None
print(parse_and_double("10")) # 20
print(parse_and_double("abc")) # Could not process 'abc': ... / None
print(parse_and_double(None)) # Could not process 'None': ... / NonePassing a non-numeric string raises ValueError; passing None raises TypeError (since int(None) isn't valid). Both are handled identically here, since the actual response — print a message, return None — doesn't depend on which specific exception occurred.
Multiple Separate Except Blocks
Using distinct except clauses
When different exception types genuinely need different handling, stack separate except blocks instead:
try:
value = int(input("Enter a number: "))
result = 10 / value
except ValueError:
print("That wasn't a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")Order and matching behavior
Python checks except blocks top to bottom, in the order they're written, and runs only the first one that matches — exactly mirroring the elif chain evaluation order covered in the earlier conditionals article. Only one block will ever run per exception, since only one exception can actually be raised and caught per try statement (outside of the exception-group scenario covered in Section 5).
The trade-off vs. tuple grouping
Separate blocks are more verbose than a single tuple-grouped except, but they preserve real clarity about exactly what went wrong and precisely how each specific situation is being handled — a genuine advantage whenever the handling logic actually differs meaningfully between exception types. As a rule of thumb: use tuple grouping when the response is identical; use separate blocks when it isn't.
Catching (Almost) Everything and Suppressing Errors
A broad catch-all: except Exception as e
Sometimes you genuinely want a last-resort catch-all, to prevent a program from crashing on any unexpected error, even ones you didn't specifically anticipate:
try:
risky_operation()
except Exception as e:
print(f"Something unexpected happened: {e}")except Exception catches the overwhelming majority of everyday errors — but, as covered in the earlier try/except/finally article, it deliberately does not catch KeyboardInterrupt or SystemExit, since those inherit from BaseException rather than Exception specifically. This is precisely why except Exception is the meaningfully safer choice over a fully bare except:, which catches absolutely everything, including those system-level signals that a user or the runtime genuinely needs to be able to use to stop your program.
contextlib.suppress for intentional, targeted ignoring
When you genuinely want to ignore specific exceptions entirely and just move on — no logging, no fallback value, just "if this fails, that's fine, continue" — contextlib.suppress offers a cleaner, more explicit alternative to an empty except: pass block:
from contextlib import suppress
with suppress(FileNotFoundError):
import os
os.remove("temp_file.txt")
print("Continues regardless of whether the file existed")This is functionally equivalent to:
try:
import os
os.remove("temp_file.txt")
except FileNotFoundError:
passBut suppress(FileNotFoundError) states the intent more directly and readably — "deliberately ignore this specific exception type" is immediately clear from the line itself, rather than requiring a reader to notice an empty except block and infer that the omission was intentional rather than an oversight.
Best practice: only catch what you can genuinely handle
This is worth stating plainly, since it's a genuinely common anti-pattern: catching broadly purely to hide errors, rather than to meaningfully handle them, is a real problem — not a solution. except Exception: pass, scattered through a codebase without any real handling logic, silently swallows genuine bugs, making them far harder to notice and diagnose later, often surfacing as much more confusing, seemingly unrelated failures somewhere else entirely. Only catch an exception type when you have a genuine, meaningful response to it — logging it clearly, providing a sensible fallback, or retrying — not simply to make an error message disappear.
Exception Groups and except* (Python 3.11+)
The problem this solves
Everything covered so far handles the case of "one operation, one exception at a time." But some scenarios genuinely involve multiple, independent exceptions occurring together — running several concurrent tasks where more than one might fail simultaneously, or validating several unrelated fields in a form or config file, where you'd rather collect every validation failure at once, instead of stopping at the very first one encountered.
ExceptionGroup: a container for several exceptions
Python 3.11 introduced ExceptionGroup, a special exception type that can hold several distinct, unrelated exceptions together as a single unit, raised all at once:
try:
raise ExceptionGroup(
"Multiple validation errors",
[ValueError("Invalid age"), TypeError("Invalid name type")]
)
except* ValueError as eg:
print(f"Value errors: {eg.exceptions}")
except* TypeError as eg:
print(f"Type errors: {eg.exceptions}")Value errors: (ValueError('Invalid age'),)
Type errors: (TypeError('Invalid name type'),)The new except* syntax
except* (introduced alongside ExceptionGroup via PEP 654) is specifically designed to handle exception groups — unlike a regular except, which stops looking once it finds a single match, except* can match and handle multiple sub-groups within the same ExceptionGroup, splitting them out by type, all from a single raised group.
A practical example: batch config validation
def validate_config(config):
errors = []
if not isinstance(config.get("port"), int):
errors.append(TypeError("port must be an integer"))
if config.get("timeout", 0) < 0:
errors.append(ValueError("timeout cannot be negative"))
if "name" not in config:
errors.append(KeyError("name is required"))
if errors:
raise ExceptionGroup("Config validation failed", errors)
try:
validate_config({"port": "8080", "timeout": -5})
except* TypeError as eg:
print(f"Type errors: {[str(e) for e in eg.exceptions]}")
except* ValueError as eg:
print(f"Value errors: {[str(e) for e in eg.exceptions]}")
except* KeyError as eg:
print(f"Missing fields: {[str(e) for e in eg.exceptions]}")Type errors: ['port must be an integer']
Value errors: ['timeout cannot be negative']
Missing fields: ["'name is required'"]This is genuinely useful for exactly this kind of batch-validation scenario: rather than raising and catching the very first problem found, then stopping — forcing the caller to fix one issue, rerun, hit the next issue, fix that, rerun again — validate_config() collects every validation failure into a single ExceptionGroup, and the calling code can report all of them together in one pass, sorted cleanly by category via separate except* clauses.
This is a genuinely more advanced, more recent addition to Python's exception-handling toolkit — for the overwhelming majority of everyday code, the tuple-grouping and multiple-except-block patterns covered earlier in this article remain the tools you'll reach for constantly. ExceptionGroup/except* earn their place specifically in concurrent or batch-processing scenarios, where genuinely multiple, simultaneous failures are a real, expected possibility rather than an edge case.