So far we’ve caught exceptions Python raised for us. You can also raise them yourself, to signal that something has gone wrong in your own code.

The raise statement

def withdraw(balance: float, amount: float) -> float:
    if amount < 0:
        raise ValueError("Cannot withdraw a negative amount")
    if amount > balance:
        raise ValueError("Insufficient funds")
    return balance - amount


print(withdraw(100.0, 30.0))    # 70.0
print(withdraw(100.0, -5.0))    # ValueError: Cannot withdraw a negative amount

raise <Exception>(message) immediately stops the function and throws the exception. It propagates up the call stack like any other exception.

When to raise

Raise an exception when your code can’t proceed and the caller needs to know.

  • Bad input: raise ValueError("...")
  • Wrong type: raise TypeError("...")
  • Operation not supported: raise NotImplementedError(...)
  • Configuration mistake: a custom exception (next lesson)

If you can handle the situation locally, do — don’t raise. Reserve raising for things the function genuinely can’t fix on its own.

Picking the right built-in

Use the most specific built-in that fits:

  • ValueError — right type, wrong value. (int("hello"), age = -1)
  • TypeError — wrong type altogether. (len(5))
  • KeyError — missing key in a dict-like.
  • IndexError — out-of-range index.
  • RuntimeError — something happened that doesn’t fit other categories.
  • NotImplementedError — placeholder in a class for methods subclasses must override.

Avoid raise Exception("...") — it’s too generic. The caller can’t tell what to catch.

The traceback shows where you raised

def check_age(age: int) -> None:
    if age < 0:
        raise ValueError(f"Age cannot be negative: {age}")

check_age(-5)
Traceback (most recent call last):
  File "main.py", line 4, in <module>
    check_age(-5)
    ~~~~~~~~~^^^^
  File "main.py", line 3, in check_age
    raise ValueError(f"Age cannot be negative: {age}")
ValueError: Age cannot be negative: -5

Note how the error message includes the bad value. Always include the offending data in the message — it makes debugging much faster.

raise inside except — re-raise

A bare raise inside an except re-raises the current exception:

def parse(text: str) -> int:
    try:
        return int(text)
    except ValueError:
        print("logging: failed to parse")
        raise        # let the caller see the same error

This is how libraries log without swallowing the error.

raise … from … — chaining

When you catch one exception and want to raise a different one, use from to preserve the original:

class ConfigError(Exception):
    pass


def load_port(text: str) -> int:
    try:
        return int(text)
    except ValueError as e:
        raise ConfigError(f"Port must be a number, got {text!r}") from e

The resulting traceback shows both errors:

ValueError: invalid literal for int() with base 10: 'abc'

The above exception was the direct cause of the following exception:

ConfigError: Port must be a number, got 'abc'

This lets your code give a friendly, high-level error message while preserving the technical detail for debugging.

assert — a check that raises automatically

assert is a shortcut for “if this condition isn’t true, raise an error”:

def divide(a: float, b: float) -> float:
    assert b != 0, "Divisor must not be zero"
    return a / b

It’s the same as:

def divide(a: float, b: float) -> float:
    if b == 0:
        raise AssertionError("Divisor must not be zero")
    return a / b

Don’t use assert for input validation in production code. Python can be run with -O (optimisation), which silently removes all assert statements. Use if/raise for checks that matter; reserve assert for internal invariants you’re confident about — like “at this point in the algorithm, the list should have at least one item”.

Practical example — input validation

def create_user(name: str, age: int) -> dict[str, str | int]:
    if not name:
        raise ValueError("Name cannot be empty")
    if age < 0 or age > 150:
        raise ValueError(f"Age must be between 0 and 150, got {age}")
    return {"name": name, "age": age}


try:
    user = create_user("", 30)
except ValueError as e:
    print(f"Bad input: {e}")

The function raises clear errors when called incorrectly. The caller catches and reports them in a user-friendly way. That’s the typical division of responsibilities.

What’s next

You can raise built-in exceptions. Next, custom exception classes — for when no built-in fits your domain.

Toggle theme (T)