coding
raising
manually
throwing
exception
python

Manually raising (throwing) an exception in Python

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Manually raising exceptions is how Python code communicates invalid state, bad inputs, and unsupported operations in a structured way. The raise statement is not only for failures, it is also part of API design and defensive programming. Good exception usage makes debugging faster and error handling predictable.

Raise Built-in Exceptions for Common Errors

Start with built-in exception types whenever they match your scenario.

python
1def divide(a: float, b: float) -> float:
2    if b == 0:
3        raise ValueError("b must not be zero")
4    return a / b
5
6
7try:
8    print(divide(10, 0))
9except ValueError as exc:
10    print("handled:", exc)

Choosing a specific exception type matters more than a custom message alone. It lets callers catch meaningful categories instead of broad Exception.

Pick the Right Exception Type

Typical mappings in Python code:

  • ValueError for invalid value with correct type
  • TypeError for wrong argument type
  • KeyError for missing mapping key
  • RuntimeError for generic runtime state failures

Bad pattern:

python
raise Exception("something went wrong")

Better pattern:

python
raise ValueError("age must be positive")

Specific exceptions improve caller behavior and test precision.

Define Custom Exceptions for Domain Errors

When built-ins are too generic, create domain-specific exceptions.

python
1class InsufficientFundsError(Exception):
2    pass
3
4
5def withdraw(balance: int, amount: int) -> int:
6    if amount < 0:
7        raise ValueError("amount must be non-negative")
8    if amount > balance:
9        raise InsufficientFundsError("withdrawal exceeds balance")
10    return balance - amount
11
12
13try:
14    withdraw(100, 150)
15except InsufficientFundsError as exc:
16    print("domain error:", exc)

Custom types make service boundaries clearer and reduce fragile string matching in handlers.

Re-Raise While Preserving Original Traceback

If you catch an exception only to log or add context, re-raise correctly.

python
1def parse_int(value: str) -> int:
2    try:
3        return int(value)
4    except ValueError:
5        print("invalid integer input")
6        raise

raise without argument inside except preserves original traceback, which is usually what you want for debugging.

Chain Exceptions for Better Context

Use raise ... from ... when converting low-level exceptions into domain-level ones.

python
1def parse_config(raw: str) -> dict:
2    import json
3
4    try:
5        return json.loads(raw)
6    except json.JSONDecodeError as exc:
7        raise ValueError("configuration JSON is invalid") from exc

This keeps original cause available while presenting a cleaner interface to higher layers.

You can inspect full chain in traceback, which helps root-cause analysis.

Raise in Validation Layers Early

A practical pattern is fail-fast validation near input boundaries.

python
1def create_user(email: str, age: int) -> dict:
2    if "@" not in email:
3        raise ValueError("email format is invalid")
4    if age < 13:
5        raise PermissionError("minimum age requirement not met")
6
7    return {"email": email, "age": age}

Early validation keeps deeper business logic simpler and avoids partial updates with invalid data.

Raise Inside Asynchronous Code

Exception mechanics are same in async functions, but they propagate through await points.

python
1import asyncio
2
3
4async def fetch_profile(user_id: int) -> dict:
5    if user_id <= 0:
6        raise ValueError("user_id must be positive")
7    await asyncio.sleep(0.1)
8    return {"id": user_id}
9
10
11async def main() -> None:
12    try:
13        await fetch_profile(0)
14    except ValueError as exc:
15        print("async error:", exc)
16
17
18asyncio.run(main())

If not caught, async exceptions fail task execution and bubble to event loop error handling.

Testing Raised Exceptions

Unit tests should assert both type and message intent.

python
1import pytest
2
3
4def test_divide_zero() -> None:
5    with pytest.raises(ValueError, match="must not be zero"):
6        divide(5, 0)

This guards behavior and avoids accidental weakening to generic exception types.

Logging and Raise Strategy

A common anti-pattern is logging and re-raising at many layers, producing duplicate logs. Usually log once at boundary where request is handled, then rethrow or convert as needed.

Guideline:

  • inner layers raise meaningful exceptions
  • boundary layer logs with request context
  • avoid swallowing exceptions silently

This keeps logs actionable without noise.

Common Pitfalls

A common pitfall is raising generic Exception everywhere, which makes callers over-catch and hide real bugs. Another is replacing an exception without chaining and losing root cause context. Teams also often catch broad Exception and continue execution in invalid state. Finally, using exceptions for normal control flow in tight loops can hurt readability and performance.

Summary

  • Use raise with specific exception types.
  • Prefer built-in exceptions unless domain-specific types add clear value.
  • Re-raise with bare raise to preserve traceback.
  • Use raise ... from ... for explicit causal context.
  • Keep validation and exception strategy consistent across layers.

Course illustration
Course illustration

All Rights Reserved.