Python has four ways to raise an exception inside a handler, and each one tells a different story in the traceback. Bare raise re-throws the current exception unchanged. raise X starts a new one and lets Python add the previous as implicit context. raise X from Y explicitly says "the new one was caused by the old one". raise X from None suppresses the old one entirely. Choosing well matters because tracebacks are how production bugs get diagnosed: a clean cause chain points at the real problem; a noisy or hidden chain wastes hours. This entry covers all four patterns, the __cause__, __context__, and __traceback__ attributes that produce the chain, the modern ExceptionGroup for parallel work (PEP 654), and the patterns production code actually uses. Part of our Stdlib Essentials guide.
Key Takeaways
Bare raise re-raises the current exception unchanged; the canonical "log and re-throw" pattern.
raise X starts fresh; Python sets __context__ to the active exception automatically.
raise X from Y sets __cause__ explicitly; signals "I translated this error into a new one".
raise X from None suppresses context display; reserve for clean public-API errors.
ExceptionGroup + except* (PEP 654, 3.11+) bundles multiple errors from parallel work into one raisable object.
Three raise patterns, what each produces in the traceback, and the attributes Python sets to drive the display.
The Four raise Patterns
Each pattern serves a specific intent:
Bare raise: re-raise the current exception (only valid inside an except block).
raise X: raise a new exception. If inside an except block, Python sets __context__ automatically.
raise X from Y: raise X and explicitly chain it to Y via __cause__.
raise X from None: raise X and suppress the implicit context display.
The rest of the article walks through each with examples and the traceback they produce.
Bare raise: Re-Raise the Current Exception
The most common pattern in production code: log the error and let it propagate.
def load_config(path):
try:
return read_file(path)
except FileNotFoundError:
log.error(f"config missing at {path}")
raise # let the caller decide what to do
The bare raise re-raises the currently-handled exception with the original traceback intact. Callers see exactly where the error originated; logs capture the local context.
Important properties:
Bare raise is valid ONLY inside an except block (or in a function called from one). Using it elsewhere raises RuntimeError: No active exception to re-raise.
The traceback is preserved exactly. You don't lose any frames.
This is the right pattern when you handle the error partially (log, audit, increment a counter) but don't have a better exception to throw.
Don't write raise e to "re-raise". Catching as except SomeError as e and doing raise e creates a new exception object and changes the traceback in subtle ways. Use bare raise for re-raising.
raise X: New Exception with Implicit Context
When you want to throw a different exception type, just raise X inside the except block. Python automatically chains the original via __context__:
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: 'twelve'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
RuntimeError: age parsing failed
The "During handling of the above exception, another exception occurred" message tells the reader that the second error happened while the first was being handled. This is automatic; you don't write any chaining code.
This is the right pattern when the new exception is incidental to the old one (something failed during cleanup, or you're raising a different kind of error in a finally block). It's NOT the right pattern when you're deliberately translating one error into another; for that, use raise X from Y.
raise X from Y: Explicit Cause Chain
When you catch a low-level error and want to surface a higher-level one, signal the relationship explicitly with from:
class ConfigError(Exception):
pass
def load_config(path):
try:
return json.loads(open(path).read())
except (FileNotFoundError, json.JSONDecodeError) as e:
raise ConfigError(f"config at {path} unreadable") from e
The traceback shows:
Traceback (most recent call last):
...
json.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):
...
ConfigError: config at /etc/myapp.conf unreadable
"The above exception was the direct cause" tells the reader you DELIBERATELY translated the low-level error into a domain-specific one. This is the right pattern for library code that wants to expose a clean error hierarchy while preserving the underlying cause for debugging.
Under the hood: raise X from Y sets X.__cause__ = Y and X.__suppress_context__ = True. The implicit context is still recorded in __context__ but isn't shown by the default traceback formatter when __suppress_context__ is True. The behavior comes from PEP 3134 (Exception Chaining).
raise X from None: Suppress the Original
Use from None when the original exception's detail would distract or confuse the caller:
def validate_email(s: str) -> str:
try:
local, domain = s.split("@", 1)
except ValueError:
raise ValueError(f"not a valid email: {s!r}") from None
if "." not in domain:
raise ValueError(f"domain missing a dot: {s!r}")
return s
The traceback shows only your clean ValueError, not the ValueError: not enough values to unpack that str.split raised internally. The original is still accessible via __context__ if a debugger inspects, but the default display is cleaner.
The PEP 409 rationale: library code often wraps low-level exceptions into more meaningful ones, and showing both layers can clutter logs without adding value. Reserve from None for cases where the implementation detail genuinely doesn't help the caller. Overusing it makes debugging harder when something unexpected goes wrong.
The Underlying Attributes
Four attributes drive the display:
Attribute
What it holds
How it's set
__traceback__
The traceback object showing where the exception was raised
Automatic on every raise
__context__
The exception that was being handled when this one was raised
Automatic if raised inside an except block
__cause__
The exception declared as the explicit cause via from
Set by raise X from Y
__suppress_context__
If True, default traceback hides __context__
Set to True by from X and from None
You can inspect them at runtime for custom error handling:
try:
do_something()
except MyError as e:
if e.__cause__ is not None:
log.error(f"caused by {e.__cause__}")
elif e.__context__ is not None:
log.error(f"during handling of {e.__context__}")
Logging frameworks and APM tools rely on these attributes to produce useful diagnostic output.
ExceptionGroup and except* (Python 3.11+)
PEP 654 introduced ExceptionGroup as a way to raise multiple exceptions as one, and except* as syntax for handling specific types inside the group.
def process_all(items):
errors = []
for item in items:
try:
process(item)
except Exception as e:
errors.append(e)
if errors:
raise ExceptionGroup("some items failed", errors)
try:
process_all(items)
except* ValueError as eg:
# handle ValueError instances in the group
for err in eg.exceptions:
log.warning(f"value error: {err}")
except* TypeError as eg:
for err in eg.exceptions:
log.error(f"type error: {err}")
The most common consumer is asyncio.TaskGroup (covered in the async/await guide), which collects failures from concurrent tasks. For application code, you mostly receive ExceptionGroups from libraries; for library code running parallel work, raising an ExceptionGroup is the modern correct pattern.
Real-World Patterns
Library wrapping low-level errors
class DatabaseError(Exception):
pass
class ConnectionFailed(DatabaseError):
pass
def connect(url):
try:
return _driver.connect(url)
except (socket.error, OSError) as e:
raise ConnectionFailed(f"could not connect to {url}") from e
Callers catch DatabaseError for general DB failures or ConnectionFailed specifically; the underlying OSError remains visible via __cause__ for debugging.
CLI tool with friendly errors
def main():
try:
run_pipeline(args)
except FileNotFoundError as e:
raise SystemExit(f"input file not found: {e.filename}") from None
except PermissionError as e:
raise SystemExit(f"permission denied: {e.filename}") from None
from None hides the stack trace for expected user errors; SystemExit exits with code 1 and prints the message cleanly.
Log and re-raise
def critical_section():
try:
do_work()
except Exception:
log.exception("critical_section failed")
raise # let the caller decide
log.exception records the full traceback before the bare raise propagates the error upward.
Common Mistakes
1. Writing raise e instead of bare raise
except SomeError as e:
raise e # WRONG - creates a new exception, traceback altered
except SomeError:
raise # RIGHT - re-raises the current exception unchanged
The bare form is canonical. raise e works but isn't equivalent and is harder to read.
2. Forgetting to chain low-level errors
Throwing a domain exception without from e loses the underlying cause:
except OSError:
raise ConfigError("can't read config") # __context__ holds OSError but no explicit cause
For wrapped errors, use from e to make the relationship explicit.
3. Overusing raise from None
Suppressing context hides debugging information. Use it only for clean public APIs where the implementation detail genuinely doesn't help.
4. Catching bare Exception
except Exception: # too broad
raise CustomError("something went wrong") from None
This swallows everything including keyboard interrupts and programming bugs. Catch specific exception types.
Silent failures hide problems. Either re-raise after logging or handle the error meaningfully (return a sensible default with clear documentation).
Frequently Asked Questions
How do I re-raise the current exception in Python?
Use bare raise inside an except block. It re-raises the currently-handled exception, preserving the original traceback so debuggers and logs show where the error truly originated. Pattern: try: do_thing() except SomeError: log_and_handle(); raise. Don't use raise SomeError() to "re-raise" the same type; that creates a new exception object and may lose context. Bare raise is the canonical re-raise; it works only inside an except block (using it elsewhere raises RuntimeError because there's no current exception to re-raise).
What is the difference between raise X from Y and just raise X in Python?
raise X from Y sets the new exception's __cause__ attribute to Y, producing "The above exception was the direct cause of the following exception:" in tracebacks. Plain raise X (inside an except block) sets __context__ implicitly to the currently-handled exception, producing "During handling of the above exception, another exception occurred:". The practical difference: __cause__ signals "I deliberately translated this error into a higher-level one", while __context__ shows "a second error happened while we were dealing with the first". Use raise X from Y when wrapping a low-level error into a domain-specific one; rely on the implicit context when the second exception is incidental.
When should I use raise X from None in Python?
Use raise X from None when the original exception's detail would distract or confuse the caller. Common cases: translating a generic OSError into a clean ValidationError where the OSError chain adds noise; hiding implementation details in a library public API; surfacing a user-friendly error in a CLI tool where stack traces from internal subsystems aren't useful. The from None part sets __suppress_context__ to True so the traceback shows only your new exception. The original is still accessible via __context__ for debugging, just not printed by default. Use sparingly: hiding context can make debugging harder when something unexpected goes wrong.
What is the difference between __cause__ and __context__ in Python exceptions?
__context__ is set automatically when an exception is raised inside an except block; Python records the currently-handled exception as the new one's context. __cause__ is set explicitly via raise X from Y; it signals deliberate causation. Both attributes can hold the original exception, but they appear differently in tracebacks: __cause__ produces "The above exception was the direct cause of the following exception:", while __context__ produces "During handling of the above exception, another exception occurred:". The __suppress_context__ attribute, set to True by raise X from None, tells the traceback formatter to skip the context display. All three are documented in PEP 3134 (exception chaining).
What are ExceptionGroup and except* in Python 3.11?
PEP 654 introduced ExceptionGroup as a way to bundle multiple exceptions into a single raisable object, and except* (with the asterisk) as syntax for handling specific exception types from within a group. The motivating use case is asyncio.TaskGroup, where multiple concurrent tasks can each raise their own error; the group collects them and re-raises as one ExceptionGroup. The except* syntax filters: except* ValueError handles only the ValueError instances in the group and leaves the rest. For application code, you mostly consume ExceptionGroups from libraries (TaskGroup, parallel processing). For library code that runs many things in parallel, raising an ExceptionGroup with all the failures is the modern correct pattern.
The Bottom Line: Bare raise to pass through, raise from for wrapping, from None sparingly
Bare raise is the canonical re-throw inside an except block. raise X creates a new exception and chains the active one via __context__ automatically. raise X from Y explicitly signals deliberate translation. raise X from None suppresses the original context for clean public-API errors. ExceptionGroup + except* bundle multiple errors from parallel work (PEP 654, 3.11+). Pick by intent; the traceback shape tells the next developer (or your future self) what actually happened. Next: running shell commands with subprocess. Or browse the full Stdlib Essentials guide.
Build Exception-Handling Reflexes
CodeGym's Python track turns exception patterns into reflex across 62 levels: bare raise re-throw, raise from wrapping, ExceptionGroup for parallel work, traceback-aware logging. AI validators catch the raise-e anti-pattern and the from-None overuse; AI mentors explain when each pattern is the right tool. First level free; full plan on the pricing page.
GO TO FULL VERSION