Handling Multiple Exceptions in Python: Grouping, Ordering, and Exception Groups
Most real functions can fail in more than one way. A file-loading function might encounter a missing file, permission denied, or invalid content. Handling multiple exception types correctly means knowing how to write distinct handlers for each, how to group related exceptions, and how to order them so the right handler fires.
Separate except Blocks for Different Failures
The most explicit approach: one except block per exception type, each with its own response.
def load_user_data(path): try: with open(path) as f: import json data = json.load(f) return data except FileNotFoundError: print(f"File not found: {path}") return None except PermissionError: print(f"Permission denied reading: {path}") return None except json.JSONDecodeError as e: print(f"Invalid JSON in {path} at line {e.lineno}: {e.msg}") return None except OSError as e: print(f"OS error reading {path}: {e}") return None
result = load_user_data("users.json")Each failure mode has a specific, meaningful message. Callers who call load_user_data get a None in every failure case, but the printed message tells them why.
Grouping Exceptions in a Tuple
When two or more exception types should receive identical handling, group them in a single except clause using a tuple.
def parse_input(value): try: return int(value) except (ValueError, TypeError) as e: print(f"Cannot convert {value!r} to int: {e}") return 0
print(parse_input("42")) # 42print(parse_input("hello")) # Cannot convert 'hello' to int: ...print(parse_input(None)) # Cannot convert None to int: ...ValueError handles the case where the string cannot be converted (int("hello")). TypeError handles passing something that isnβt a string or number at all (int(None)). Since both lead to the same fallback (return 0), grouping them is clean.
Ordering Matters: Specific Before General
Pythonβs exception hierarchy is a class tree. If you list a parent exception before its child, the parentβs handler will always fire and the childβs handler will never be reached.
class AppError(Exception): pass
class DatabaseError(AppError): pass
class ConnectionError(DatabaseError): pass
# Wrong order β DatabaseError catches ConnectionError firsttry: raise ConnectionError("host unreachable")except DatabaseError: print("Database error") # fires β ConnectionError IS-A DatabaseErrorexcept ConnectionError: print("Connection error") # unreachable
# Correct order β most specific firsttry: raise ConnectionError("host unreachable")except ConnectionError: print("Connection error") # fires correctlyexcept DatabaseError: print("Database error") # fallback for other DB errorsexcept AppError: print("App error") # fallback for everything elseRule: more specific exceptions come before less specific ones. When in doubt, check the inheritance chain.
Using Exception as a Catch-All
At the top level of a program β like a web request handler or a job runner β catching Exception broadly can prevent one failing job from crashing the whole process.
def run_job(job_id, func, *args): try: result = func(*args) print(f"Job {job_id} succeeded: {result}") return result except Exception as e: # Log the full traceback, return a failure signal import traceback print(f"Job {job_id} failed: {type(e).__name__}: {e}") traceback.print_exc() return NoneEven here, be precise about what you do with the error. Logging the traceback and continuing is fine. Silently ignoring it (except Exception: pass) is not.
Do not catch BaseException β it includes KeyboardInterrupt and SystemExit, which should propagate to let the program exit cleanly.
Re-raising After Catching
Sometimes you want to log or annotate an exception and then let it continue propagating.
import logging
def process_payment(amount): try: charge_card(amount) except PaymentGatewayError as e: logging.error(f"Payment failed for Β£{amount}: {e}") raise # re-raises the same exception with the same tracebackPlain raise (with no arguments) re-raises the current exception. The traceback points to the original source, not to this function.
Python 3.11+: ExceptionGroup for Concurrent Errors
Python 3.11 introduced ExceptionGroup for situations where multiple exceptions occur simultaneously β common in async code and task runners.
# Python 3.11+def run_batch(tasks): errors = [] results = [] for task in tasks: try: results.append(task()) except Exception as e: errors.append(e)
if errors: raise ExceptionGroup("batch processing failed", errors) return results
try: run_batch([...])except* ValueError as eg: print(f"Value errors: {eg.exceptions}")except* IOError as eg: print(f"IO errors: {eg.exceptions}")The except* syntax handles each exception type within the group independently. An ExceptionGroup can be partially handled β some sub-exceptions are caught while others propagate.
Practical Example: Multiple Failure Modes
import csvimport os
def import_csv(path, required_columns): """Load a CSV and validate it has the required columns.""" if not os.path.exists(path): raise FileNotFoundError(f"No file at {path}")
try: with open(path, newline="") as f: reader = csv.DictReader(f) rows = list(reader) except PermissionError: raise PermissionError(f"Cannot read {path} β check permissions") from None except csv.Error as e: raise ValueError(f"Malformed CSV in {path}: {e}") from e
if not rows: raise ValueError(f"CSV file {path} is empty")
missing = required_columns - set(rows[0].keys()) if missing: raise ValueError(f"CSV missing required columns: {missing}")
return rows
# Caller handles each failure type appropriatelytry: data = import_csv("contacts.csv", {"name", "email", "phone"})except FileNotFoundError as e: print(f"Upload failed β file missing: {e}")except PermissionError as e: print(f"Upload failed β access denied: {e}")except ValueError as e: print(f"Upload failed β bad data: {e}")Common Mistakes
Listing a general exception before a specific one. The more specific one will never match.
Grouping unrelated exceptions that need different handling. Grouping ValueError and ConnectionError in one block means you cannot give them different responses.
Forgetting that except matches subclasses. Catching Exception catches everything that inherits from it β which is almost all exceptions. Catching OSError catches FileNotFoundError, PermissionError, and others.
Using multiple bare except: clauses. Python allows only one bare except: per try block, and it must come last. Multiple specific except ExceptionType: clauses are fine.