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)) # 25set_age(-5) # raises ValueErrorset_age("twenty") # raises TypeErrorYou 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.
| Exception | Use when |
|---|---|
ValueError | Right type, wrong value (int("abc"), negative age) |
TypeError | Wrong type entirely (len(42)) |
KeyError | Dictionary key not found |
IndexError | List index out of range |
FileNotFoundError | File or directory does not exist |
PermissionError | Insufficient permissions |
RuntimeError | Something went wrong that doesnβt fit a more specific category |
NotImplementedError | Abstract 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 / bWriting 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 helpfulraise ValueError("Invalid input")
# Specific β usefulraise 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 eThe 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 Nonefrom 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 valuedef 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 itdef find_user(user_id): user = db.get(user_id) if user is None: raise UserNotFoundError(f"No user with id {user_id!r}") return userThe 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.