Skip to main content

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

Next up in your learning path