Python Tutorial
Raising Exceptions and Custom Exceptions
Handling errors is half the story; the other half is signalling them clearly. When a function receives invalid input or cannot do its job, it should raise an exception with a precise type and a helpful message, rather than returning None or -1 that callers might ignore.
This lesson covers the raise statement, re-raising, exception chaining with raise ... from, designing custom exception hierarchies for your application, adding data to exceptions, and notes with add_note().
raise
raise ValueError("age must be positive") stops the function and sends the exception up the call stack until something handles it. Choose the most fitting built-in type: ValueError (right type, bad value), TypeError (wrong type), KeyError, LookupError, PermissionError, NotImplementedError, RuntimeError. A bare raise inside an except block re-raises the current exception after, for example, logging it.
Exception Chaining
When you catch a low-level error and raise a higher-level one, write raise NewError(...) from original. The traceback then shows both, with "The above exception was the direct cause of the following exception". Use from None to hide an irrelevant original error.
Custom Exceptions
Define your own exceptions by subclassing Exception. A good pattern is one base class for your application (ShopError) with specific subclasses (OutOfStockError, PaymentDeclinedError). Callers can then catch a specific error or all errors from your code at once. Custom exceptions can carry extra attributes such as an order id or an error code for API responses.
Adding Context with add_note()
Since Python 3.11, exception.add_note("...") attaches extra context that appears in the traceback — useful when re-raising errors while processing a particular file or record.
Examples
Raising built-in exceptions with clear messages
def set_age(age):
if not isinstance(age, int):
raise TypeError(f"age must be an int, got {type(age).__name__}")
if not 0 <= age <= 130:
raise ValueError(f"age must be between 0 and 130, got {age}")
return age
for value in [25, -3, "25"]:
try:
print("ok:", set_age(value))
except (TypeError, ValueError) as e:
print(f"{type(e).__name__}: {e}")
ok: 25
ValueError: age must be between 0 and 130, got -3
TypeError: age must be an int, got str
Re-raising and exception chaining with from
import json
class ConfigError(Exception):
pass
def load_config(text):
try:
return json.loads(text)
except json.JSONDecodeError as e:
raise ConfigError("config file is not valid JSON") from e
try:
load_config("{bad json")
except ConfigError as e:
print("error:", e)
print("caused by:", type(e.__cause__).__name__, "-", e.__cause__.msg)
def risky():
try:
1 / 0
except ZeroDivisionError:
print("logging the problem, then re-raising")
raise
try:
risky()
except ZeroDivisionError as e:
print("caller received:", e)
error: config file is not valid JSON
caused by: JSONDecodeError - Expecting property name enclosed in double quotes
logging the problem, then re-raising
caller received: division by zero
A custom exception hierarchy with extra data
class ShopError(Exception):
"""Base class for all shop errors."""
class OutOfStockError(ShopError):
def __init__(self, item, requested, available):
super().__init__(f"only {available} {item}(s) left, {requested} requested")
self.item, self.requested, self.available = item, requested, available
class PaymentDeclinedError(ShopError):
def __init__(self, reason, code="CARD_DECLINED"):
super().__init__(reason)
self.code = code
stock = {"hoodie": 2}
def buy(item, qty, card_ok=True):
if stock.get(item, 0) < qty:
raise OutOfStockError(item, qty, stock.get(item, 0))
if not card_ok:
raise PaymentDeclinedError("insufficient balance")
stock[item] -= qty
return "order placed"
for args in [("hoodie", 1), ("hoodie", 5), ("hoodie", 1, False)]:
try:
print(buy(*args))
except OutOfStockError as e:
print("stock:", e, "| available =", e.available)
except ShopError as e:
print(f"shop error [{getattr(e, 'code', '-')}]:", e)
order placed
stock: only 1 hoodie(s) left, 5 requested | available = 1
shop error [CARD_DECLINED]: insufficient balance
Adding context with add_note()
rows = ["10", "20", "abc", "40"]
try:
for line_no, row in enumerate(rows, start=1):
try:
int(row)
except ValueError as e:
e.add_note(f"while processing line {line_no} of marks.csv")
raise
except ValueError as e:
print(e)
print(e.__notes__)
invalid literal for int() with base 10: 'abc'
['while processing line 3 of marks.csv']
Common Mistakes
- Returning None or -1 for errors instead of raising, so failures go unnoticed.
- Raising the generic Exception instead of a specific type.
- Raising a new exception inside except without "from", losing or confusing the original cause.
- Deriving custom exceptions from BaseException instead of Exception.
Key Points to Remember
- raise SpecificError("clear message") signals problems precisely.
- A bare raise re-raises the current exception.
- raise New(...) from original chains exceptions and keeps the cause.
- Build custom exception hierarchies from Exception, with extra attributes when useful.
- add_note() attaches extra context (3.11+).
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.