Chapters
Python114 chapters

Files and errorsChapter 84 of 114

Raising Exceptions

Signal a problem yourself, and define your own exception types.

raise

Python
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:

Python
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:

SituationRaise
right type, impossible valueValueError
wrong type altogetherTypeError
a key or name is missingKeyError
the operation makes no sense nowRuntimeError
not written yetNotImplementedError
Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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 questions

Why 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.

Next chapter

CSV Files

Read and write comma-separated data without breaking on quoted commas.