Python has no checked exceptions. Nothing forces you to handle anything, which makes the decisions about what to catch entirely yours. This post covers the mechanics and the judgement.
try and except
def parse_amount(text):
try:
return float(text)
except ValueError:
return None
print(parse_amount("15.50")) # Output: 15.5
print(parse_amount("fifteen")) # Output: None
Code that might fail goes in try; the recovery goes in except. Name the
exception you expect — that is the whole discipline. If something other than ValueError
happens, it propagates, which is what you want for a bug you have not thought about.
Never write a bare except
def total(rows):
try:
return sum(float(r) for r in rows)
except: # catches EVERYTHING
return 0
print(total(["1", "2"])) # Output: 3.0
print(total(None)) # Output: 0
The second call did not fail because of bad data — None is not iterable, which is a
programming mistake. The bare except turned it into 0 and the program carried
on with a wrong total.
A bare except also catches KeyboardInterrupt, so Ctrl-C stops
working. If you genuinely must catch broadly, use except Exception, which at least excludes
the ones that mean "stop the program" — and log it rather than swallowing it.
Catching more than one
import json
def load(text):
try:
return json.loads(text)["balance"]
except (json.JSONDecodeError, KeyError) as error:
return f"bad input: {type(error).__name__}"
print(load('{"balance": 100}')) # Output: 100
print(load('not json')) # Output: bad input: JSONDecodeError
print(load('{"other": 1}')) # Output: bad input: KeyError
A tuple handles several the same way; separate except blocks handle them differently.
as error binds the exception object so you can inspect or log it.
Order matters: except blocks are tried top to bottom and the first matching one wins, so
a broad type above a narrow one makes the narrow one unreachable.
else and finally
def withdraw(balance, amount):
try:
amount = float(amount)
except ValueError:
print("not a number")
else:
print(f"withdrawing {amount}") # only if nothing raised
finally:
print("done") # always
withdraw(100, "25")
withdraw(100, "abc")
# Output: withdrawing 25.0
# Output: done
# Output: not a number
# Output: done
else runs only when the try succeeded. Its value is keeping the
try block down to the one line that might fail — anything else in there could raise the
same exception and be caught by mistake.
finally always runs, exception or not, return or not. It is for cleanup. In practice
with replaces most uses of it, which is the next
post.
Raising your own
class InsufficientFundsError(Exception):
def __init__(self, requested, available):
super().__init__(
f"Insufficient funds: asked for ${requested:,.2f}, "
f"only ${available:,.2f} available."
)
self.requested = requested
self.available = available
@property
def shortfall(self):
return self.requested - self.available
try:
raise InsufficientFundsError(250, 100)
except InsufficientFundsError as error:
print(error) # Output: Insufficient funds: asked for $250.00, only $100.00 available.
print(error.shortfall) # Output: 150
Subclass Exception, never BaseException. Calling
super().__init__(message) is what makes str(error) work — forget it and your
exception prints as an empty string.
Carrying the numbers as attributes means a caller that wants them does not have to parse your message. That is the difference between an exception that reports and one that can be handled.
A base class per application
The bank app defines one root so a single except catches every rule it enforces:
class BankError(Exception):
...
class AuthenticationError(BankError):
...
class InsufficientFundsError(BankError):
...
except BankError at the menu catches every deliberate failure and nothing else. Without
the shared base you either list every subclass or fall back to except Exception, which
swallows your own bugs along with them.
raise ... from
When you catch a low-level error and re-raise a meaningful one, keep the original attached:
from decimal import Decimal, InvalidOperation
def parse_money(text):
try:
return Decimal(text)
except InvalidOperation as error:
raise ValueError(f"'{text}' is not an amount") from error
try:
parse_money("abc")
except ValueError as error:
print(error) # Output: 'abc' is not an amount
print(type(error.__cause__).__name__) # Output: InvalidOperation
The caller gets an error in their own vocabulary; the traceback still shows what really happened, under "The above exception was the direct cause of the following exception".
Leave out from error and Python prints "During handling of the above exception, another
exception occurred" — a subtly different message meaning the second error was accidental. Say which one
you meant.
Reading the traceback
An uncaught exception prints a traceback, and it is the most useful thing on your screen. Read it from the bottom up:
Traceback (most recent call last):
File "bank.py", line 40, in <module>
main()
File "bank.py", line 31, in main
withdraw(account, "abc")
File "bank.py", line 22, in withdraw
amount = float(text)
^^^^^^^^^^^
ValueError: could not convert string to float: 'abc'
The last line is what went wrong. The block above it is the call chain that got there, oldest at the
top — so the frame just above the error is the line that actually failed, and the frames above that are
how you arrived. From 3.11 the ^^^^ markers underline the exact expression, not just the
line.
The habit worth building is to read the final line first, then scan upwards for the topmost frame that is your code rather than a library's. That is nearly always where the fix belongs.
What not to catch
The reflex to wrap everything in try is worth resisting. An exception you cannot do
anything useful about should travel — the traceback that reaches you is far more informative than a
None returned from three functions down.
Catch when you have a genuine recovery: a default to fall back on, a message to show the user, a retry to attempt. Otherwise let it raise.
Python's habit is to try the operation rather than check first — "easier to ask forgiveness than
permission". float(text) in a try is more idiomatic than a regex that predicts
whether it will work, because the conversion is the authority on that question.
Next: File (read/write).