Raising Exceptions in Python
Error Handling
Raising Exceptions the Right Way
Use built-in exception types—or create custom ones—to provide meaningful errors.
Raising built-in exceptions
Pick the most specific built-in that fits: ValueError for a bad value, TypeError for a wrong type, KeyError/LookupError for failed lookups. An actionable message states the rule that was broken and the offending value:
def set_price(value):
if value < 0:
raise ValueError(f"price must be >= 0, got {value}")
set_price(-5)
# ValueError: price must be >= 0, got -5
if not user.active:
raise PermissionError(f"user {user.id} is inactive")
"Invalid input" forces whoever hits the error to add print statements. "price must be >= 0, got -5" tells them what to fix immediately — include IDs and values, not just adjectives.
Re-raising
To log or clean up but still let the error propagate, use a bare raise inside except — it re-raises the in-flight exception with its traceback untouched:
def handle(order):
try:
charge(order)
except ConnectionError:
logger.warning("charge failed, order %s left unpaid", order.id)
raise # same exception, original traceback preserved
raise e also works but appends the raise e line as an extra frame in the traceback, pointing readers at your handler instead of the real failure. Prefer bare raise.
Exception chaining
Raising inside an except block automatically links the new exception to the old one. Without from, Python reports it as implicit context:
try:
url = {}["api_url"]
except KeyError:
raise RuntimeError("config incomplete")
Traceback (most recent call last):
File "app.py", line 2, in <module>
url = {}["api_url"]
~~^^^^^^^^^^^
KeyError: 'api_url'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "app.py", line 4, in <module>
raise RuntimeError("config incomplete")
RuntimeError: config incomplete
Add from exc when the new exception is a deliberate translation of the old one — the header changes to say so:
import json
raw = "not json"
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError("Invalid payload format") from exc
Traceback (most recent call last):
File "app.py", line 5, in <module>
payload = json.loads(raw)
^^^^^^^^^^^^^^^
... # json's internal frames omitted
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "app.py", line 7, in <module>
raise ValueError("Invalid payload format") from exc
ValueError: Invalid payload format
Either way the original traceback survives, which makes debugging far easier — see reading stack traces for how to walk chained output.
Suppressing context with from None
When the inner exception is an implementation detail that would only confuse callers, from None hides it:
SETTINGS = {"debug": False}
def get_setting(name):
try:
return SETTINGS[name]
except KeyError:
raise ValueError(f"unknown setting: {name!r}") from None
get_setting("verbose")
# ValueError: unknown setting: 'verbose' (no KeyError shown)
Raise or return None?
Raise when the caller cannot sensibly continue; return None only when absence is a normal, expected outcome. A silently returned None fails later — usually as an AttributeError far from the real cause:
def find_user(user_id): # absence is normal -> None is fine
return USERS.get(user_id)
def load_user(user_id): # absence is a bug -> fail loudly, here
try:
return USERS[user_id]
except KeyError:
raise LookupError(f"no user with id {user_id!r}") from None
assert is not raise
assert exists for internal invariants — conditions that can only be false if your code is wrong. Running Python with -O strips every assert, so it must never guard user input or external data:
def withdraw(amount):
# WRONG for validation: vanishes under `python -O`
assert amount > 0, "amount must be positive"
# right: always enforced
if amount <= 0:
raise ValueError(f"amount must be positive, got {amount}")
Frequently Asked Questions
What is the difference between raise, raise e, and raise ... from e?
A bare raise (only valid inside except) re-raises the current exception unchanged — the best way to re-raise. raise e re-raises it too but adds your handler line as an extra traceback frame. raise NewError(...) from e raises a different exception and marks e as its direct cause, keeping both tracebacks; from None does the opposite and hides the original.
When should a function raise instead of returning None?
Raise when the failure means the caller cannot proceed correctly — invalid arguments, broken invariants, unavailable resources. Return None (or a sentinel) only when "not found / not set" is an ordinary answer the caller is expected to check, like dict.get. If callers would just crash later on the None, raising at the source produces a far clearer traceback.
Can I raise a string in Python?
No. Everything you raise must be an exception class or instance deriving from BaseException. String exceptions were removed long ago (Python 2.6); raising one today is itself a TypeError.
raise "something went wrong"
# TypeError: exceptions must derive from BaseException