Course topics

By WebNest Studio

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

Python
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}")
Output
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

Python
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)
Output
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

Python
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)
Output
order placed
stock: only 1 hoodie(s) left, 5 requested | available = 1
shop error [CARD_DECLINED]: insufficient balance

Adding context with add_note()

Python
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__)
Output
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.