🐍 Python - Exceptions
Exceptions are the primary error handling method in Python.
raise Exception('error message')
# ... stacktrace ...
# Exception: error message
phonebook = {
'Alice': 123,
'Bob': 456,
}
number = None
try:
number = int(phonebook['David'])
except (KeyError, ValueError):
pass # actually you should maybe log a warning
assert number is None
Confusingly, exceptions are also used for signaling. It's easier to ask for forgiveness than permission so exceptions are also used as flow control. In many other languages, exceptions are solely for problems that need to handled, but this not the case in Python.
for-loop iteration uses next() to get the next value, if no value
is available, it raises StopIteration exception; it's a signal, not an error
try/else
will run if no exceptions are raised.
def dangerous_call(crash: bool) -> None:
if crash:
raise ValueError('value is unacceptable')
try:
dangerous_call(crash=True)
except ValueError as exc:
assert True
else:
assert False
try:
dangerous_call(crash=False)
except ValueError as exc:
assert False
else:
assert True
If you call raise
inside except
, it will re-raise the caught exception.
def dangerous_call() -> None:
raise ValueError('value is unacceptable')
try:
dangerous_call()
except ValueError:
raise
# => ValueError: value is unacceptable
Make your exception classes self-documenting. Custom exception classes make it easy to understand what's going on when things go wrong. Extra points if you also tell how to fix the issue.
class NameTooShortError(Exception):
pass
def validate(name: str) -> None:
count = 5
if len(name) < count:
raise NameTooShortError(
f'name "{name}" is too short, use more than {count} letters'
)
validate('Alexander')
validate('John')
# => NameTooShortError: name "John" is too short, use more than 5 letters
Define a base exception class for your library. Allows catching only errors related to the library.
class BaseError(Exception):
pass
class NameTooShortError(BaseError):
pass
class NameTooLongError(BaseError):
pass
def validate(name: str) -> None:
if len(name) < 5:
raise NameTooShortError(name)
if len(name) > 10:
raise NameTooLongError(name)
try:
validate('Esmeralda')
except BaseError as err:
print("problem from your library", err)
except Exception as err:
print("lower-level problem", err)
Most exception messages are in lowercase in the standard library. Consider following this convention but no one will blink an eye of you won't. The important thing is to be consistent with your own errors.
names = []
names.pop()
# => raise IndexError('pop from empty list')
# IndexError: pop from empty list
names[10]
# => raise IndexError('list index out of range')
# IndexError: list index out of range
You may want to add more information to your custom exceptions. Consider making additional information optional, otherwise you need to specify __reduce__
method into your exception class to make it pickleable.
from typing import List, Optional
OListStr = Optional[List[str]]
class ValidationError(Exception):
def __init__(self, message: str, reasons: OListStr = None) -> None:
super().__init__(message)
self.reasons = reasons if reasons else []
def validate(name: str) -> None:
errors = []
if len(name) < 5:
errors.append('is too short')
if any(char.isdigit() for char in name):
errors.append('contains a number')
if errors:
raise ValidationError(f'name "{name}" is invalid,', errors)
for n in ['John', '4l']:
try:
validate(n)
except ValidationError as err:
print(err, err.reasons)
# => name "John" is invalid, ['is too short']
# => name "4l" is invalid, ['is too short', 'contains a number']
# you can show these to users as you wish (if you wish)
Minimize code in try
-except
blocks. Gives more room to raise unexpected exception.
try:
doSomethingThatCanRaiseError()
except SomeError:
FixIt()
except (UsError, ThemError): # catching multiple exceptions
FixThem()
else: # no errors
doOtherStuff()
finally: # error or not, will be ran last
finalizeStuff()
Never catch all exceptions silently. Don't use blank except:
or catching Exception
or StandardError
, except you are logging or printing the error on the outermost block.
try:
doSomethingThatCanRaiseError()
except:
# the above is bad
pass
Exceptions have 3 interesting dunder properties: __context__
, __cause__
and __traceback__
__context__
defines that while handling the "context" exception, current exception was raised.__cause__
defines that due to the "cause" exception, current exception was raised.__traceback__
records the function call stack for debugging.
In more technical terms:
__context__
is set if youraise
inside anexcept
handler, can beNone
.__cause__
is only set if you useraise ... from exception
, can beNone
.- If
__cause__
is set, so is__supress_context__
and__context__
is ignored. raise ... from None
can be used to hide any context or cause.
try:
raise ValueError()
except ValueError:
raise IndexError()
# ... ValueError
# During handling of the above exception, another exception occurred:
# ... IndexError
try:
raise ValueError()
except ValueError as ve:
raise IndexError() from ve
# ... ValueError
# The above exception was the direct cause of the following exception:
# ... IndexError
Sources
- Style Guide For Python Code
- Django Coding Style
- Google Python Guide
- Python Newbie Mistakes
- Learn Python in Y minutes
- Python’s null equivalent: None
- Python Idioms
- Python Best Practice Patterns
- Become More Advanced: Master the 10 Most Common Python Programming Problems
- Python Tricks The Book, Dan Bader
- Fluent Python, Luciano Ramalho