Why this matters
So far exceptions have happened to you. Now you will raise them yourself. When your function receives input it cannot work with, the worst thing it can do is carry on and return something wrong, or a "magic" value like -1 that the caller forgets to check. Raising an exception stops the bad data immediately, at the point where it was detected, with a message that explains exactly what was wrong. Defining your own exception types lets callers react precisely to your failures. This is how every serious library reports errors, and it is how your own programs will stay debuggable as they grow.
raise
The raise statement takes an exception object (or class) and starts the same unwinding process you saw when Python raises an error itself:
def set_volume(level):
if not 0 <= level <= 10:
raise ValueError(f"volume must be 0-10, got {level}")
print("volume set to", level)
set_volume(7)
set_volume(42)
volume set to 7
Traceback (most recent call last):
File "/work/main.py", line 7, in <module>
set_volume(42)
File "/work/main.py", line 3, in set_volume
raise ValueError(f"volume must be 0-10, got {level}")
ValueError: volume must be 0-10, got 42
ValueError(...) creates the exception object (it is a class call, like int("3")); raise raises it. The function stops at that line: nothing after the raise runs, and no value is returned. raise ValueError without parentheses also works (Python creates the object for you), but always include a message: the person reading the traceback will thank you.
Which type to raise
Reuse built-in types when they fit; callers already know them:
| Situation | Raise |
|---|---|
| Argument has the wrong type | TypeError |
| Right type, unacceptable value | ValueError |
| Missing key in a mapping-like lookup | KeyError |
| Index out of range | IndexError |
| Feature intentionally not implemented yet | NotImplementedError |
| Something in the environment failed (files, OS) | OSError subclasses, usually raised for you |
A good message says what was expected and what was received: "expected a date like 2026-11-01, got '11/01'" beats "bad date".
Raise early, at the boundary
Validate inputs at the start of a function, before doing any work. The error then points at the real cause, and you never leave things half done:
def average(nums):
if not nums:
raise ValueError("average() of an empty list")
return sum(nums) / len(nums)
Compare the alternative, ZeroDivisionError: division by zero from inside the formula: technically true, but it says nothing about what the caller did wrong.
Defining your own exception classes
Classes are module 12's topic. For exceptions you need only one line of class syntax:
class InventoryError(Exception):
pass
Read it as "define a new class named InventoryError that is a kind of Exception". The name in parentheses is the base class; the new class inherits everything from it (storing the message, printing, being catchable). pass is the empty body, since nothing needs to be added. By convention, exception class names end in Error.
You raise and catch your class like a built-in one:
class OutOfStock(Exception):
pass
stock = {"apple": 3, "pear": 0}
def take(item):
if stock.get(item, 0) == 0:
raise OutOfStock(f"no {item} left")
stock[item] -= 1
for item in ["apple", "pear"]:
try:
take(item)
print("took", item)
except OutOfStock as e:
print("sorry:", e)
took apple
sorry: no pear left
Why bother instead of raising ValueError? Because a caller can now catch exactly this situation without accidentally catching an unrelated ValueError from somewhere deeper in the code.
A small hierarchy
Libraries usually define one base exception and specific subclasses:
class ShopError(Exception):
"""Base class for all errors raised by the shop module."""
class OutOfStock(ShopError):
pass
class PaymentDeclined(ShopError):
pass
(A docstring can serve as the body instead of pass.) Callers choose their precision: except OutOfStock: for one case, except ShopError: for "anything the shop module complains about", while bugs such as TypeError still propagate and get noticed.
Exception
└── ShopError
├── OutOfStock
└── PaymentDeclined
You can also inherit from a more specific built-in, for example class BadDateError(ValueError), so that existing code catching ValueError still works.
Exception chaining: raise ... from
Often you catch a low-level error and raise a higher-level one that makes sense to your caller. Do it with raise NewError(...) from original:
def load_port(text):
try:
return int(text)
except ValueError as err:
raise ConfigError(f"port must be a number, got {text!r}") from err
The traceback then shows both, connected:
Traceback (most recent call last):
File "/work/main.py", line 6, in load_port
return int(text)
ValueError: invalid literal for int() with base 10: 'http'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/work/main.py", line 11, in <module>
load_port("http")
File "/work/main.py", line 8, in load_port
raise ConfigError(f"port must be a number, got {text!r}") from err
ConfigError: port must be a number, got 'http'
The new exception's __cause__ attribute holds the original. Nothing is lost: the reader sees the meaningful message and the technical root cause.
If you raise a new exception inside a handler without from, Python still records the original, in __context__, but prints a different connecting line: "During handling of the above exception, another exception occurred". That wording suggests a bug in the handler. So use from err when the translation is intentional. To deliberately hide the original (rarely wise), write raise NewError(...) from None.
What raise does underneath
raise asks the VM to start unwinding with the given object. The VM attaches a traceback object to the exception, recording the current frame and line, and adds one entry for every frame it passes through on the way up. That is how the printed traceback knows every call in the chain, and why it is available later as e.__traceback__. If a handler catches it, unwinding stops in that frame and execution continues in the handler.
Common mistakes
Raising a string.
raise "something went wrong"
TypeError: exceptions must derive from BaseException
Raise an exception object: raise RuntimeError("something went wrong").
Inheriting from BaseException. Your classes should derive from Exception (or a subclass). Deriving from BaseException means except Exception: handlers will not catch them, like Ctrl-C.
Returning error codes instead. return -1 or return None for failure pushes the checking onto every caller, and forgetting it produces wrong results far from the cause. Raise.
Vague messages. raise ValueError("invalid") forces the reader to add print statements to learn what was invalid. Include the bad value with !r.
Forgetting from. Re-raising a translated error without from err produces the misleading "During handling..." text.
Recap
raise SomeError("message")stops the function and unwinds the stack; validate inputs early and raise with a message that names the bad value.- Reuse built-ins when they fit:
TypeErrorfor wrong types,ValueErrorfor bad values. class MyError(Exception): passdefines your own exception; a base class plus subclasses lets callers catch broadly or narrowly.raise New(...) from errchains exceptions, keeping the original as__cause__; withoutfrom, it is kept as__context__with a "during handling" message.
Going further: read PEP 3134 ("Exception Chaining and Embedded Tracebacks"), and in the REPL raise a chained exception and inspect e.__cause__, e.__context__ and e.__traceback__.tb_lineno.