Technology  /  Python

🐍 Python 78 guides · updated 2026

From first variable to OOP, generators, and real projects β€” the language that runs everything from data pipelines to AI agents, taught the practical way.

Raising Exceptions in Python: When, How, and How to Add Useful Context

When your code detects a problem, you have two choices: return a special value to signal failure, or raise an exception. Python strongly favours exceptions: they are impossible to ignore silently, they carry context, and they propagate up the call stack until something handles them.

The Basic raise Statement

def set_age(age):
if not isinstance(age, int):
raise TypeError(f"Age must be an integer, got {type(age).__name__}")
if age < 0 or age > 150:
raise ValueError(f"Age {age} is outside the valid range 0-150")
return age
print(set_age(25)) # 25
set_age(-5) # raises ValueError
set_age("twenty") # raises TypeError

You raise an exception by calling its class with a message string. The message is the first thing you (and callers) see in the traceback. Make it specific: include the invalid value, the constraint that was violated, and ideally a hint about what to do instead.

Choosing the Right Exception Type

Python has a rich hierarchy of built-in exceptions. Using the correct one helps callers write precise except clauses.

ExceptionUse when
ValueErrorRight type, wrong value (int("abc"), negative age)
TypeErrorWrong type entirely (len(42))
KeyErrorDictionary key not found
IndexErrorList index out of range
FileNotFoundErrorFile or directory does not exist
PermissionErrorInsufficient permissions
RuntimeErrorSomething went wrong that doesn’t fit a more specific category
NotImplementedErrorAbstract method or planned feature not yet built
def divide(a, b):
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError(f"Expected numbers, got {type(a).__name__} and {type(b).__name__}")
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b

Writing Useful Error Messages

A good error message answers three questions: what went wrong, what value caused it, and (when possible) what the caller should do instead.

# Vague β€” not helpful
raise ValueError("Invalid input")
# Specific β€” useful
raise ValueError(
f"Username {username!r} contains invalid characters. "
f"Only letters, digits, and underscores are allowed."
)

The !r format specifier wraps the value in quotes (via repr()), which is helpful when the value could be whitespace, None, or an empty string β€” otherwise it might look like nothing in the message.

Re-raising Exceptions

Inside an except block, plain raise (with no arguments) re-raises the current exception without modifying it. Use this when you want to log or count the error but still let it propagate.

import logging
def fetch_data(url):
try:
return download(url)
except TimeoutError:
logging.warning(f"Timeout fetching {url} β€” will retry")
raise # same exception, same traceback, continues propagating
def download_with_retry(url, attempts=3):
for attempt in range(1, attempts + 1):
try:
return fetch_data(url)
except TimeoutError:
if attempt == attempts:
raise # last attempt β€” propagate the exception
print(f"Attempt {attempt} failed, retrying...")

Exception Chaining with raise from

When you catch one exception and want to raise a different one β€” perhaps to add context or convert a low-level error into a domain-specific one β€” use raise NewException(...) from original_exception.

class ConfigurationError(Exception):
pass
def load_settings(path):
try:
with open(path) as f:
import json
return json.load(f)
except FileNotFoundError as e:
raise ConfigurationError(
f"Settings file not found at {path!r}. "
f"Run 'python setup.py init' to create it."
) from e
except json.JSONDecodeError as e:
raise ConfigurationError(
f"Settings file at {path!r} contains invalid JSON "
f"(line {e.lineno}, column {e.colno})"
) from e

The from e part sets ConfigurationError.__cause__ = e, so anyone who inspects the traceback can see both the high-level problem and its root cause.

ConfigurationError: Settings file not found at 'config.json'.
Run 'python setup.py init' to create it.
The above exception was the direct cause of the following exception:
...
FileNotFoundError: [Errno 2] No such file or directory: 'config.json'

Suppressing the Original Exception

Sometimes you want to raise a new exception without showing the original as the cause β€” for example, to avoid leaking internal implementation details in a library.

try:
result = internal_function()
except _InternalError:
raise PublicAPIError("Operation failed") from None

from None sets __cause__ to None and suppresses the chained traceback.

When to Raise vs When to Return a Sentinel

This is a design decision, but the Python community’s preference is clear: use exceptions for errors, not return values.

# Anti-pattern: caller might not check the return value
def find_user(user_id):
user = db.get(user_id)
if user is None:
return None # easy to forget to check
# Better: exception forces the caller to handle it
def find_user(user_id):
user = db.get(user_id)
if user is None:
raise UserNotFoundError(f"No user with id {user_id!r}")
return user

The exception approach is especially valuable when the function is called many levels deep β€” the error propagates automatically, without each intermediate function checking and forwarding a sentinel.

Raising NotImplementedError for Abstract Methods

Without the abc module, you can signal unimplemented methods by raising NotImplementedError:

class Formatter:
def format(self, data):
raise NotImplementedError(
f"{type(self).__name__} must implement format()"
)
class JSONFormatter(Formatter):
def format(self, data):
import json
return json.dumps(data)

Common Mistakes

Raising a string instead of an exception class. raise "something went wrong" is a TypeError β€” you must raise an exception instance or class.

Losing the original exception. Using raise NewError(...) without from original loses the original traceback. Callers and logs lose context about the root cause.

Raising at the wrong level. Low-level code should raise low-level exceptions. High-level code should catch them and raise domain-specific ones. The conversion should happen at the boundary, not scattered everywhere.

Using assert for validation instead of raise. assert statements are disabled when Python runs with the -O (optimise) flag. Never use assert to enforce a function contract β€” use raise with an appropriate exception type.