Skip to content
← All Python articles

Python explainer

Keep the Original Error with raise from

About 4 min to read. Allow extra time to work through examples.

Replace a low-level error with your own and keep the original attached with raise from, read the two lines that join chained tracebacks, and add a note when you want to keep the error type.

This article uses Python. You can make it your practice mode to browse related content across the site.

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 an except block.
  • __cause__ is the error you named with from. Without from, it is None.

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

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.

step 5 of 20

Error Handling with Exceptions, Retries and Warnings

Up next Wrap a Low-Level Error with raise from