Your app reads its port number from a settings file. If the file says
80a, then int("80a") raises a ValueError. The code that calls read_port is simpler if every bad setting raises one
error type, such as a ConfigError of your own. So you catch the ValueError
and raise a ConfigError instead. Write from exc after it, and Python
keeps the original error attached:
class ConfigError(Exception):
pass
def read_port(text: str) -> int:
try:
return int(text)
except ValueError as exc:
raise ConfigError("port must be a whole number") from exc
read_port("80a")
Save it as port.py and run it. Python 3.15 prints both errors:
Traceback (most recent call last):
File "port.py", line 7, in read_port
return int(text)
ValueError: invalid literal for int() with base 10: '80a'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "port.py", line 12, in <module>
read_port("80a")
~~~~~~~~~^^^^^^^
File "port.py", line 9, in read_port
raise ConfigError("port must be a whole number") from exc
ConfigError: port must be a whole number
This is a chained exception, one error that led to another. Read it as
two tracebacks. The bottom one is the ConfigError that stopped the
program. The one above is the ValueError that caused the ConfigError, and
its message shows the bad value. The line between them, "The above exception was the direct
cause of the following exception:", says that you raised the ConfigError
because of the ValueError.
Without from, the error looks like a bug in your handler
Leave out from exc, and Python still keeps the ValueError. But it joins
the two tracebacks with a different line:
During handling of the above exception, another exception occurred:
That line says a second error happened while your except block ran, as if
the handler itself had a bug. Write from exc whenever you replace one
error with another on purpose.
Two attributes hold the original
Replace the last line of the example with this, and run it again:
try:
read_port("80a")
except ConfigError as error:
print(repr(error.__cause__))
print(error.__context__ is error.__cause__)
ValueError("invalid literal for int() with base 10: '80a'")
True
__context__is the error your code was handling when it raised the new one. Python sets it on its own whenever you raise inside anexceptblock.__cause__is the error you named withfrom. Withoutfrom, it isNone.
Here both point at the same ValueError. When there is no earlier error,
both are None, so check for None before you read the class of either.
Hide the original with from None
Sometimes the original error adds nothing for the person reading the
traceback. Write raise ConfigError(...) from None, and the traceback shows
only the ConfigError. Python still stores the original in __context__,
and error-reporting tools can read it from there. So keep passwords and keys
out of exception messages, whichever form you use.
Re-raise the same error with a bare raise
Sometimes you catch an error only to log it, and the caller should still
see it unchanged. Write raise on its own inside the except block. Python
re-raises the same exception object with its traceback.
Add a note instead of wrapping
To add context but keep the error type, call add_note() and re-raise:
def parse_amounts(rows: list[str]) -> list[int]:
amounts: list[int] = []
for row_number, text in enumerate(rows, start=1):
try:
amounts.append(int(text))
except ValueError as exc:
exc.add_note(f"reading amount on row {row_number}")
raise
return amounts
print(parse_amounts(["12", "unknown", "7"]))
The traceback ends with the message and then the note:
ValueError: invalid literal for int() with base 10: 'unknown'
reading amount on row 2
Check your understanding Why add a note here instead of raising a new BatchError from the ValueError? Show answer Hide answer
Callers can still catch ValueError, because the exception is the same object with one more line of text. A new BatchError would change which except clauses match. Use raise ... from when you want callers to see your own error type. Use add_note() when you only want to explain the same error better. A note is text for people, so put data that code needs in an attribute instead.
Next, in Wrap a Low-Level Error with raise from, you turn a KeyError into
a ConfigError. Then you check which of the two lines the traceback prints.