When wrapping a library error, what does `raise MyError(...) from exc` preserve?
answer
- Something is attached to the new exception
- Two attributes get set, not one
- Explicit cause versus implicit context
- `__cause__` and `__suppress_context__`
- `from None` is the deliberate opposite
basics
~20 sThe original exception, attached to the new one as its __cause__. Both errors are then printed, so the domain type reaches the caller while the library's real message and stack frames stay available to whoever reads the log.
solid answer
~40 s`from exc` sets `__cause__` on the exception being raised to the object named after `from`, and sets `__suppress_context__` to `True`. Practically, translation stops being lossy: the caller sees and catches your domain type, while the traceback still prints the driver's or parser's original error underneath it, and a handler can inspect `err.__cause__` programmatically. Omitting `from` inside an `except` block does not actually delete the original - Python records it implicitly as `__context__` - but it reads as an accident, and it is the explicit form that says "I translated this on purpose". The deliberate opposite is `raise MyError(...) from None`, which drops the link when the underlying error is noise the caller must never see, such as a stack trace that would expose an internal path or credential.
code
python · 16 linesclass ConfigError(Exception):
pass
def parse_port(raw):
try:
return int(raw)
except ValueError as exc:
raise ConfigError(f"bad port: {raw!r}") from exc
try:
parse_port("eighty")
except ConfigError as err:
print("cause:", repr(err.__cause__))
print("suppress_context:", err.__suppress_context__)go deeper
Remember the shape: inside an except ... as exc block, write raise MyError(...) from exc. Know that the original is not thrown away - it is attached and still printed under your error.
Explain what the statement sets - __cause__ and __suppress_context__ - and how explicit cause differs from the implicit __context__ you get without from, including the two different sentences the traceback prints.
Show when you would deliberately break the chain with from None, what you log before doing so, and how you report several failures from one boundary rather than wrapping only the last one.
Own the convention across services: whether wrappers may ever drop causes, what must be logged at a boundary before an error is sanitised for a caller, and how much of an internal chain is allowed to reach an external consumer.
## The problem `from` solves Translating an error means the caller stops seeing the thing that actually went wrong. That is the point at the API boundary - and a problem at three in the morning, when the useful sentence is the driver's "database is locked", not your "report store is unavailable". `raise ... from exc` resolves the tension: the caller gets your type, the log keeps their message. Concretely, `raise MyError("...") from exc` does two things to the new exception object before it propagates: * sets `__cause__` to `exc`; * sets `__suppress_context__` to `True`. Both are ordinary attributes you can read afterwards. A handler, or a test at the boundary, can assert that the wrapper it caught actually came from the failure it expected: ```python try: load_settings(text) except ConfigError as err: assert isinstance(err.__cause__, ValueError) ``` ## What happens if you leave `from` out Raising inside an `except` block without `from` does not throw the original away. Python attaches it implicitly as `__context__`, and the display still shows it - under the wording "During handling of the above exception, another exception occurred" rather than "The above exception was the direct cause of the following exception". So the information survives either way. The difference is intent, and it matters more than it looks. "During handling ... another exception occurred" is the phrasing you get when a handler itself crashed - a bug. A deliberate translation that reads like an accident sends every reader of that traceback looking for a second fault that does not exist. Writing `from exc` is how you say the second exception is the first one, restated in your vocabulary. ## The deliberate opposite: `from None` `raise MyError("...") from None` sets `__cause__` to `None` and suppresses the context, so only your exception is displayed. It is the right call in a narrow set of cases: * the underlying error is pure noise - a parse failure whose message repeats what yours already says; * the underlying error would expose something the caller must not see, such as a filesystem path, a connection string or a credential embedded in a driver message; * a public boundary where a long internal chain would only confuse. Use it consciously. `from None` is the one form of translation that really is lossy, and reaching for it to make tracebacks look tidy is how teams lose the only evidence of a failure. If the reason is secrecy rather than noise, a better move is often to log the original at the boundary and raise the clean error to the caller. ## Translating without flattening Two mistakes come up constantly. The first is `raise MyError(str(exc)) from exc` - copying the original's text into your message. Now the same sentence appears twice in the traceback, and callers start pattern-matching on a string that belongs to a library you intended to be able to replace. Say what failed in your own words; let `__cause__` carry theirs. The second is wrapping in a loop or a fan-out and keeping only the last failure. When a boundary performs several independent operations, one wrapper exception with one `__cause__` reports one of the failures and hides the rest. Since Python 3.11 the honest structure is an exception group, raised so that the caller can handle the members with `except*`; before that, teams carried a list of failures on the wrapper as an attribute. ## Adding context rather than replacing it Sometimes the right answer is not to translate at all but to annotate. Since Python 3.11 every exception has `add_note()`, which attaches a string that is printed with the traceback. When the underlying type is already the right type for the caller - an `OSError` from a file the caller named - adding "while writing tonight's report" as a note is better than inventing a wrapper: the caller's `except OSError` keeps working and the traceback gains the missing fact. ## The rule of thumb Inside an `except` block, always be explicit. Use `from exc` when translating, `from None` when you have decided the original must not be shown, and treat a bare `raise NewError(...)` inside a handler as a smell worth a second look - it is either a translation missing its `from`, or a genuine second failure that deserves its own investigation.
- If omitting `from` still records the original as `__context__`, why bother writing it?Because the two render with different sentences and mean different things. Implicit context prints "During handling of the above exception, another exception occurred", which is the signature of a handler that itself blew up - a bug. Explicit cause prints "The above exception was the direct cause of the following exception", which says the translation was intentional. Readers triage tracebacks by that line, and `from exc` also sets `__suppress_context__`, keeping the chain to what you meant.
- When is `raise ... from None` the right choice at a boundary?When the original would leak something the caller must not see - a path, a connection string, a token embedded in a driver message - or when it is pure noise that repeats your own message. Even then, log the original at the boundary before dropping it, or you have destroyed the only record of the real failure. Anything else, keep the cause.
- Your boundary runs several independent operations and some fail. What do you raise?A single wrapper with one `__cause__` reports one failure and hides the others. Since Python 3.11 the right structure is an exception group carrying every failure, which callers handle with `except*` and can filter by type. If a group is too heavy for the API, attach the collected failures to your domain exception as an explicit attribute so nothing is silently dropped.
saying these in an interview costs you the question
- Says a bare raise inside except destroys the original
- Copies the original's message into the wrapper's message
- Uses from None routinely to keep tracebacks tidy
- Thinks from exc re-raises the original as well
- Cannot name __cause__ as an inspectable attribute
- Wraps a fan-out and keeps only the last failure