Files and errorsChapter 84 of 114
Raising Exceptions
Signal a problem yourself, and define your own exception types.
raise
def set_age(age):
if age < 0:
raise ValueError("age cannot be negative")
return age
print(set_age(30))
try:
set_age(-1)
except ValueError as problem:
print("ValueError:", problem)Output
30 ValueError: age cannot be negative
Raising stops the function immediately, like return, but it travels up until something catches it.
Fail early
Checking arguments at the top means an invalid object never exists, and the error points at the real cause rather than somewhere later:
class Rectangle:
def __init__(self, width, height):
if width <= 0 or height <= 0:
raise ValueError("sides must be positive")
self.width = width
self.height = height
try:
Rectangle(3, -1)
except ValueError as problem:
print("ValueError:", problem)Output
ValueError: sides must be positive
Pick the right type
Use a builtin when one fits. Readers already know what it means:
| Situation | Raise |
|---|---|
| right type, impossible value | ValueError |
| wrong type altogether | TypeError |
| a key or name is missing | KeyError |
| the operation makes no sense now | RuntimeError |
| not written yet | NotImplementedError |
def repeat(text, times):
if not isinstance(times, int):
raise TypeError("times must be an integer")
if times < 0:
raise ValueError("times cannot be negative")
return text * times
for args in [("ab", 2), ("ab", "2"), ("ab", -1)]:
try:
print(repeat(*args))
except (TypeError, ValueError) as problem:
print(type(problem).__name__ + ":", problem)Output
abab TypeError: times must be an integer ValueError: times cannot be negative
Write a useful message
The message is read by whoever hits the bug, often at three in the morning. Include the offending value:
def load(name):
known = ["ada", "grace"]
if name not in known:
raise KeyError(f"unknown person {name!r}, expected one of {known}")
try:
load("alan")
except KeyError as problem:
print(problem)Output
"unknown person 'alan', expected one of ['ada', 'grace']"
KeyError shows its message with quotes around it, which is a quirk of that one exception rather than something you did.
Custom exceptions
Define your own when callers need to catch your problem specifically:
class ConfigError(Exception):
"""The configuration file could not be used."""
def load(settings):
if "name" not in settings:
raise ConfigError("no name in the configuration")
return settings["name"]
try:
load({})
except ConfigError as problem:
print("ConfigError:", problem)Output
ConfigError: no name in the configuration
A class with a docstring and nothing else is a complete exception. Inherit from Exception, not BaseException, so it does not catch KeyboardInterrupt.
A shared base lets callers catch the whole family or one member:
class AppError(Exception):
pass
class MissingField(AppError):
pass
class BadValue(AppError):
pass
for problem in [MissingField("no name"), BadValue("age is -1")]:
try:
raise problem
except AppError as caught:
print(type(caught).__name__, "->", caught)Output
MissingField -> no name BadValue -> age is -1
Re-raising
Catch, do something, and let it continue on its way:
def load(text):
try:
return int(text)
except ValueError:
print("logging the failure")
raise
try:
load("abc")
except ValueError:
print("caller still saw it")Output
logging the failure caller still saw it
A bare raise inside an except re-raises the original, keeping its traceback.
raise ... from
When you translate one exception into another, from keeps the original attached so the traceback shows both:
class ConfigError(Exception):
pass
def load(text):
try:
return int(text)
except ValueError as problem:
raise ConfigError(f"bad port {text!r}") from problem
try:
load("abc")
except ConfigError as problem:
print(problem)
print("caused by:", type(problem.__cause__).__name__)Output
bad port 'abc' caused by: ValueError
assert is not for validation
assert checks something you believe is already true, and is removed entirely when Python runs with -O:
def average(values):
assert len(values) > 0, "internal error: empty list"
return sum(values) / len(values)
print(average([1, 2, 3]))Output
2.0
Use it for internal sanity checks in development. For anything a user can trigger — bad input, a missing file — raise a real exception, because it will still be there in production.
Test yourself
2 questionsWhy should assert not be used to validate user input?
Show the answer
Assertions are removed entirely when Python runs with -O — Use it for internal sanity checks. For anything a user can trigger, raise a real exception.
What does 'raise X from problem' add?
Show the answer
Keeps the original exception attached so the traceback shows both — A bare raise inside an except re-raises the original with its traceback intact.
CSV Files
Read and write comma-separated data without breaking on quoted commas.