Custom exceptions let a caller catch exactly the failure your code knows about, instead of a generic Exception that could have come from anywhere. Exception chaining — the raise ... from ... syntax — then lets you keep the original error visible while adding your own explanation of what it means in context.
Both ideas rest on a simple fact: an exception is just a class. Subclass Exception and you have made a new kind of error. The two clauses raise and except then work with it exactly as they do with the built-ins.
Why would you define your own exception classes?
Because except ValueError is a promise you cannot keep. If your function raises ValueError for bad input, a caller cannot tell that apart from a ValueError raised deep inside a library it called. A named exception type says precisely which layer produced the failure.
It also lets you build a small hierarchy. A caller who wants to handle everything you can raise catches the base class; a caller who cares only about one case catches the specific subclass.
class InventoryError(Exception):
"""Base class for every error this package raises."""
class InsufficientStock(InventoryError):
pass
class UnknownProduct(InventoryError):
pass
class Stock:
def __init__(self, counts):
self.counts = counts
def reserve(self, sku, amount):
if sku not in self.counts:
raise UnknownProduct(f"no product with id {sku!r}")
if self.counts[sku] < amount:
raise InsufficientStock(
f"only {self.counts[sku]} of {sku!r} left, wanted {amount}"
)
self.counts[sku] -= amount
return self.counts[sku]
stock = Stock({"A-1": 3})
try:
stock.reserve("A-1", 10)
except InsufficientStock as exc:
print("specific handler:", exc)
except InventoryError:
print("would catch any error from this package")
There are no rules about names, but conventions help: exception class names end in Error or Exception, and they are usually defined at module level so callers can import them. Keep the base class as plain Exception unless you have a specific reason to derive from something else — inheriting from ValueError is occasionally useful when you want the new type to satisfy an existing except ValueError in code you do not control.
| Exception type | Raise it when |
|---|---|
ValueError | the value is the right type but the wrong content |
TypeError | the value is not the right type at all |
KeyError | a lookup found nothing |
RuntimeError | the operation cannot continue in the current state |
How do you raise a custom exception with a useful message?
Instantiate it with a message and pass it to raise. The constructor accepts any number of arguments and stores them in args, so str(exc) gives you the message and repr(exc) shows the type and arguments together.
class ValidationError(Exception):
def __init__(self, field, value, reason):
super().__init__(f"{field}={value!r} {reason}")
self.field = field
self.value = value
try:
raise ValidationError("temperature", -300, "is below the minimum of 0")
except ValidationError as exc:
print("message :", exc)
print("field :", exc.field)
print("value :", exc.value)
Give the message the information a person debugging the failure would need — the offending value, the limit that was exceeded, the name that was not found. "Invalid input" is rarely enough; "temperature -300 is below the allowed minimum of 0" is.
You can also give your exception class its own constructor if it needs more than a message, and you can add attributes so the catcher can inspect the details programmatically rather than parsing the message string.
What does 'raise ... from ...' change about a traceback?
When an exception is raised while another is being handled, Python automatically attaches the original as an implicit background detail — you see "During handling of the above exception, another exception occurred". The from keyword replaces that implicit link with an explicit one and changes the wording to "The above exception was the direct cause of the following exception".
def load_settings(path):
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except OSError as exc:
raise RuntimeError(f"could not load settings from {path!r}") from exc
try:
load_settings("missing.ini")
except RuntimeError as exc:
print("message:", exc)
print("cause :", type(exc.__cause__).__name__)
print("cause msg:", exc.__cause__)
The important difference is not the wording, it is the __cause__ attribute. With an explicit from, the original exception is stored as __cause__, and the traceback renders both, with the cause first. A caller can inspect exc.__cause__ to get at the underlying failure programmatically.
There is a fourth possibility worth knowing. Leave the from keyword out and the original error is still attached, but as __context__ rather than __cause__. Both attributes exist on every exception; __context__ is set automatically, __cause__ only when you ask for it.
How do you hide the original error when you want to?
raise NewError("message") from None suppresses the context entirely. The traceback shows only the new exception, and __suppress_context__ becomes True. This is the right tool when the underlying error is noise to the reader — a retry that succeeded on the second attempt, or a low-level parse error you have already translated into something meaningful.
def read_number(text):
try:
return int(text)
except ValueError:
raise ValueError(f"{text!r} is not a whole number") from None
try:
read_number("twelve")
except ValueError as exc:
print("clean message:", exc)
print("cause is hidden:", exc.__cause__ is None)
Use it sparingly. Suppressing a cause destroys the trail back to the original failure, and the next person to debug that code will find it much harder. If the original error carries information someone will need, keep it and chain it explicitly.
When should you let an exception propagate untouched?
Most of the time. If a lower layer already produces a clear, accurate message, wrapping it adds a layer of noise rather than clarity. Chain when you are adding genuinely new information — the operation that failed, the identifier involved, the remediation the caller should try.
And if the fix belongs at the call site, let the exception travel. A function that catches an error only to re-raise the identical error is doing nothing but adding a stack frame.
def parse_port(text):
return int(text)
def start_server(text):
try:
port = parse_port(text)
except ValueError as exc:
raise RuntimeError(f"invalid port {text!r}") from exc
return f"listening on {port}"
try:
start_server("not-a-port")
except RuntimeError as exc:
print(exc)
print("caused by:", repr(exc.__cause__))
Next steps
- Subclass
Exceptionto make a named error type callers can catch precisely. - Build a small hierarchy — a base class for the family, subclasses for specific cases.
- Use
raise ... from originalto state the causal link explicitly and set__cause__. - Use
from Noneonly when the original context would genuinely mislead the reader. - Do not wrap an exception just to re-raise the same information. Let it propagate.
Try it yourself: a domain error hierarchy
Design an exception hierarchy for a small inventory system: a base InventoryError, a subclass for "not enough stock", another for "unknown product id", and another for "the quantity must be a positive integer".
Then write a function that validates a restock order and raises the right type in each case, with messages that name the offending product and the numbers involved. Write a caller that catches the base class once and prints the message, then write a second caller that catches only InsufficientStock and retries with a smaller quantity. Finally, chain one of your errors from an underlying KeyError and print both __cause__ and __context__ to see how Python stores them differently.