Python Multiple except Clauses and Exception Chaining Explained
Handle multiple exception types in one try block with separate except clauses or tuple syntax. Chain exceptions with raise X from Y to preserve error context.
I've been looking at the order processor code. It's got a try-except, but the except just says Exception: and then logs everything. That feels like we're hiding real bugs.
You're right. A bare except Exception is a fire extinguisher you wave at a fire without looking at what's burning. Let me show you the order processor problem first.
Okay so it takes an order dict, validates the total field, tries to convert it to a float. But the total could be missing — KeyError. The total could be bad data — ValueError. Or we could have a custom error if the order violates business rules.
Three different exceptions, three different responses. One of them means "data structure is broken," another means "data is malformed," the third means "business rule violation." The calling code needs to know which one happened. Catching everything as Exception wipes that information away.
So instead of one big except block, I write three separate ones? Each handles one exception type?
Exactly. Here's the pattern:
class InvalidOrderError(Exception):
pass
try:
if "total" not in order:
raise KeyError("missing total field")
total = float(order["total"])
if total < 0:
raise InvalidOrderError("negative total not allowed")
except KeyError as e:
print(f"Data structure error: {e}")
except ValueError as e:
print(f"Data format error: {e}")
except InvalidOrderError as e:
print(f"Business rule error: {e}")
Wait — KeyError from the "total" not in order check? That's explicit. Wouldn't only the float() raise ValueError?
Good catch. I was setting the stage. In real code, you might get KeyError from dict access — order["total"] directly instead of checking first. The explicit check here is cleaner, but both paths exist. The point: ValueError comes from float(), KeyError from dict access, InvalidOrderError from your validation. Three different exceptions, three different handlers.
Order matters though. Do I catch specific first, or general first?
Specific first. Python checks them top to bottom and runs the first one that matches. If you had:
try:
# something that raises ValueError
except Exception as e: # too general — catches everything
handle_it()
except ValueError as e: # never reached — ValueError matched by Exception first
handle_it_differently()
Then ValueError never gets to its handler because Exception caught it first.
So the rule is specific exceptions first, then more general ones.
And Exception is the most general (besides BaseException, which you should never catch). Put it last if you have it at all.
But three except blocks feels verbose. There's got to be a shortcut.
Yes — tuple syntax. When multiple exceptions need the same handler:
try:
total = float(order.get("total", None))
if total is None:
raise ValueError("total is missing")
except (KeyError, ValueError) as e:
print(f"Data error: {e}")
except InvalidOrderError as e:
print(f"Business rule error: {e}")
So (KeyError, ValueError) means "catch either of these, run the same handler for both."
Right. Only use tuple syntax when they actually need the same handling. If they need different logging or different recovery, separate blocks.
Okay but here's what's nagging me. I catch ValueError from float(), but now I've lost the original reason for the error. The ValueError message from float() just says "could not convert string to float: xyz". I've caught it, I'm handling it — but what was the real problem?
Now you're asking the right question. This is where exception chaining comes in. When you catch a low-level error and re-raise as a domain error, you want to keep the original error in the chain so someone debugging can see the root cause.
Re-raise? As a domain error?
Look at this:
class InvalidOrderError(Exception):
pass
try:
total = float(order.get("total"))
if total < 0:
raise InvalidOrderError("negative total")
except ValueError as e:
# The low-level error happened. Re-raise as domain error, but keep the chain.
raise InvalidOrderError("total field is not a valid number") from e
The from e part is the chain. It means "this InvalidOrderError happened because of that ValueError."
So I'm catching ValueError, but instead of handling it there, I'm converting it to InvalidOrderError and throwing that instead?
Right. The calling code doesn't care about ValueError. It cares about InvalidOrderError — that's the domain language. But the chain preserves the cause. When someone reads the traceback, they see: "During handling of the above exception, another exception occurred" with both the original error and the new one.
Why would I want the original error visible if I'm handling it?
Picture this: Your code raises InvalidOrderError. The web handler catches it and returns a 400 to the client — "order is invalid." But six months later, there's a bug in the order processing. Orders are failing with InvalidOrderError but nobody knows why. The developers dig into the logs and see the chain — "InvalidOrderError because ValueError: could not convert string to float: 'foo.bar.baz'". That foo.bar.baz string in the original error tells you exactly what bad data the system received. Without the chain, you're debugging blind.
Oh. So the chain is for debugging. It links the symptom (InvalidOrderError) to the root cause (ValueError).
That's exactly it.
But what if I catch the ValueError and want to suppress the chain? Like, I handled it, I'm moving on, don't bother the caller with the details?
You use from None instead of from e:
ex cept ValueError as e:
raise InvalidOrderError("total is invalid") from None
That tells Python "yes, I'm raising a new exception, but there was no prior exception." Clean break in the chain. Most of the time though, you want to keep the chain — it's valuable information.
Alright so let me read a real traceback. You said "During handling of the above exception, another exception occurred." Show me what that looks like.
Here's the output of code that chains exceptions:
Traceback (most recent call last):
File "order.py", line 15, in process_order
total = float(order["total"])
ValueError: could not convert string to float: 'abc'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "app.py", line 42, in handle_request
result = process_order(order_data)
File "order.py", line 17, in process_order
raise InvalidOrderError("order total is not a number") from e
InvalidOrderError: order total is not a number
Read from the bottom: InvalidOrderError at line 17. Read the message: "order total is not a number." Now read UP to see why: ValueError at line 15, message "could not convert string to float: 'abc'."
So the original error is the root, and the new error is the consequence.
And the chain tells you the sequence. ValueError happened first, somewhere in the try block. That triggered the except clause. The except clause raised InvalidOrderError as a response. The chain shows both.
What if there's no from? Like I just raise InvalidOrderError without chaining?
The traceback only shows InvalidOrderError. You lose the context that it was caused by ValueError. Implicit chaining still happens in some cases — Python tracks it internally — but explicit from e makes it clear in the output.
And from None is... the opposite?
It says "raise this new exception, forget the chain entirely." Only use it when the prior exception is truly irrelevant — which is rare.
Okay so in the order processor: try block with multiple raise points, multiple except clauses catching specific exception types, and when I catch low-level exceptions, I re-raise as InvalidOrderError with from e to keep the chain. That's the full pattern.
That's the pattern. Let me show it end-to-end:
class InvalidOrderError(Exception):
pass
def process_order(order):
try:
# Could raise ValueError
total = float(order.get("total"))
# Could raise InvalidOrderError
if total < 0:
raise InvalidOrderError("negative total not allowed")
return total
except ValueError as e:
# Low-level error — re-raise as domain error with chain
raise InvalidOrderError("order total is not a valid number") from e
except InvalidOrderError:
# Already our domain error — just re-raise it as-is
raise
except Exception as e:
# Unexpected error — re-raise with chain to see what happened
raise InvalidOrderError(f"unexpected error processing order: {e}") from e
So ValueError becomes InvalidOrderError with the chain. InvalidOrderError passes through unchanged. Anything else gets wrapped too. That's clean — all errors come out as InvalidOrderError from the caller's perspective, but the chain shows what really happened.
You just described the entire error handling strategy Amir uses in the order service. The calling code catches InvalidOrderError, logs it, decides how to respond. The chain is there for debugging when things go wrong.
So the rule is: specific exception types first. Tuple syntax for exceptions that need the same handler. Re-raise as domain errors with from e to preserve the chain.
That's the whole lesson. And tomorrow when we add type hints, you'll see functions decorated to return InvalidOrderError | None — the type hint documents what exceptions the function can raise, so callers know what to catch.
So exception handling chains into types?
I was going to save that for the intro tomorrow and you just jumped ahead by a day. Yes. Type hints let you document which exceptions a function can raise. The exception chain lets you see the root cause when one does.
Practice your skills
Sign up to write and run code in this lesson.
Python Multiple except Clauses and Exception Chaining Explained
Handle multiple exception types in one try block with separate except clauses or tuple syntax. Chain exceptions with raise X from Y to preserve error context.
I've been looking at the order processor code. It's got a try-except, but the except just says Exception: and then logs everything. That feels like we're hiding real bugs.
You're right. A bare except Exception is a fire extinguisher you wave at a fire without looking at what's burning. Let me show you the order processor problem first.
Okay so it takes an order dict, validates the total field, tries to convert it to a float. But the total could be missing — KeyError. The total could be bad data — ValueError. Or we could have a custom error if the order violates business rules.
Three different exceptions, three different responses. One of them means "data structure is broken," another means "data is malformed," the third means "business rule violation." The calling code needs to know which one happened. Catching everything as Exception wipes that information away.
So instead of one big except block, I write three separate ones? Each handles one exception type?
Exactly. Here's the pattern:
class InvalidOrderError(Exception):
pass
try:
if "total" not in order:
raise KeyError("missing total field")
total = float(order["total"])
if total < 0:
raise InvalidOrderError("negative total not allowed")
except KeyError as e:
print(f"Data structure error: {e}")
except ValueError as e:
print(f"Data format error: {e}")
except InvalidOrderError as e:
print(f"Business rule error: {e}")
Wait — KeyError from the "total" not in order check? That's explicit. Wouldn't only the float() raise ValueError?
Good catch. I was setting the stage. In real code, you might get KeyError from dict access — order["total"] directly instead of checking first. The explicit check here is cleaner, but both paths exist. The point: ValueError comes from float(), KeyError from dict access, InvalidOrderError from your validation. Three different exceptions, three different handlers.
Order matters though. Do I catch specific first, or general first?
Specific first. Python checks them top to bottom and runs the first one that matches. If you had:
try:
# something that raises ValueError
except Exception as e: # too general — catches everything
handle_it()
except ValueError as e: # never reached — ValueError matched by Exception first
handle_it_differently()
Then ValueError never gets to its handler because Exception caught it first.
So the rule is specific exceptions first, then more general ones.
And Exception is the most general (besides BaseException, which you should never catch). Put it last if you have it at all.
But three except blocks feels verbose. There's got to be a shortcut.
Yes — tuple syntax. When multiple exceptions need the same handler:
try:
total = float(order.get("total", None))
if total is None:
raise ValueError("total is missing")
except (KeyError, ValueError) as e:
print(f"Data error: {e}")
except InvalidOrderError as e:
print(f"Business rule error: {e}")
So (KeyError, ValueError) means "catch either of these, run the same handler for both."
Right. Only use tuple syntax when they actually need the same handling. If they need different logging or different recovery, separate blocks.
Okay but here's what's nagging me. I catch ValueError from float(), but now I've lost the original reason for the error. The ValueError message from float() just says "could not convert string to float: xyz". I've caught it, I'm handling it — but what was the real problem?
Now you're asking the right question. This is where exception chaining comes in. When you catch a low-level error and re-raise as a domain error, you want to keep the original error in the chain so someone debugging can see the root cause.
Re-raise? As a domain error?
Look at this:
class InvalidOrderError(Exception):
pass
try:
total = float(order.get("total"))
if total < 0:
raise InvalidOrderError("negative total")
except ValueError as e:
# The low-level error happened. Re-raise as domain error, but keep the chain.
raise InvalidOrderError("total field is not a valid number") from e
The from e part is the chain. It means "this InvalidOrderError happened because of that ValueError."
So I'm catching ValueError, but instead of handling it there, I'm converting it to InvalidOrderError and throwing that instead?
Right. The calling code doesn't care about ValueError. It cares about InvalidOrderError — that's the domain language. But the chain preserves the cause. When someone reads the traceback, they see: "During handling of the above exception, another exception occurred" with both the original error and the new one.
Why would I want the original error visible if I'm handling it?
Picture this: Your code raises InvalidOrderError. The web handler catches it and returns a 400 to the client — "order is invalid." But six months later, there's a bug in the order processing. Orders are failing with InvalidOrderError but nobody knows why. The developers dig into the logs and see the chain — "InvalidOrderError because ValueError: could not convert string to float: 'foo.bar.baz'". That foo.bar.baz string in the original error tells you exactly what bad data the system received. Without the chain, you're debugging blind.
Oh. So the chain is for debugging. It links the symptom (InvalidOrderError) to the root cause (ValueError).
That's exactly it.
But what if I catch the ValueError and want to suppress the chain? Like, I handled it, I'm moving on, don't bother the caller with the details?
You use from None instead of from e:
ex cept ValueError as e:
raise InvalidOrderError("total is invalid") from None
That tells Python "yes, I'm raising a new exception, but there was no prior exception." Clean break in the chain. Most of the time though, you want to keep the chain — it's valuable information.
Alright so let me read a real traceback. You said "During handling of the above exception, another exception occurred." Show me what that looks like.
Here's the output of code that chains exceptions:
Traceback (most recent call last):
File "order.py", line 15, in process_order
total = float(order["total"])
ValueError: could not convert string to float: 'abc'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "app.py", line 42, in handle_request
result = process_order(order_data)
File "order.py", line 17, in process_order
raise InvalidOrderError("order total is not a number") from e
InvalidOrderError: order total is not a number
Read from the bottom: InvalidOrderError at line 17. Read the message: "order total is not a number." Now read UP to see why: ValueError at line 15, message "could not convert string to float: 'abc'."
So the original error is the root, and the new error is the consequence.
And the chain tells you the sequence. ValueError happened first, somewhere in the try block. That triggered the except clause. The except clause raised InvalidOrderError as a response. The chain shows both.
What if there's no from? Like I just raise InvalidOrderError without chaining?
The traceback only shows InvalidOrderError. You lose the context that it was caused by ValueError. Implicit chaining still happens in some cases — Python tracks it internally — but explicit from e makes it clear in the output.
And from None is... the opposite?
It says "raise this new exception, forget the chain entirely." Only use it when the prior exception is truly irrelevant — which is rare.
Okay so in the order processor: try block with multiple raise points, multiple except clauses catching specific exception types, and when I catch low-level exceptions, I re-raise as InvalidOrderError with from e to keep the chain. That's the full pattern.
That's the pattern. Let me show it end-to-end:
class InvalidOrderError(Exception):
pass
def process_order(order):
try:
# Could raise ValueError
total = float(order.get("total"))
# Could raise InvalidOrderError
if total < 0:
raise InvalidOrderError("negative total not allowed")
return total
except ValueError as e:
# Low-level error — re-raise as domain error with chain
raise InvalidOrderError("order total is not a valid number") from e
except InvalidOrderError:
# Already our domain error — just re-raise it as-is
raise
except Exception as e:
# Unexpected error — re-raise with chain to see what happened
raise InvalidOrderError(f"unexpected error processing order: {e}") from e
So ValueError becomes InvalidOrderError with the chain. InvalidOrderError passes through unchanged. Anything else gets wrapped too. That's clean — all errors come out as InvalidOrderError from the caller's perspective, but the chain shows what really happened.
You just described the entire error handling strategy Amir uses in the order service. The calling code catches InvalidOrderError, logs it, decides how to respond. The chain is there for debugging when things go wrong.
So the rule is: specific exception types first. Tuple syntax for exceptions that need the same handler. Re-raise as domain errors with from e to preserve the chain.
That's the whole lesson. And tomorrow when we add type hints, you'll see functions decorated to return InvalidOrderError | None — the type hint documents what exceptions the function can raise, so callers know what to catch.
So exception handling chains into types?
I was going to save that for the intro tomorrow and you just jumped ahead by a day. Yes. Type hints let you document which exceptions a function can raise. The exception chain lets you see the root cause when one does.