Filter concepts by levelShowing all levels.

Python · Exception fundamentals

Exception fundamentals

The hierarchy every exception inherits from, the try/except/else/finally control flow, and raising your own

  • try / exceptcode that might fail, and the type that recovers from it
  • raise ... from ...links a new exception to the one that caused it
  • Custom exceptionssubclass Exception to carry your own structured data
  • Exception > BaseExceptionexcept Exception skips SystemExit/KeyboardInterrupt on purpose

1.Exception hierarchy

Every exception is a class inheriting from BaseException. Exception is one direct subclass of it, and almost everything you catch — ValueError, KeyError, OSError — descends from Exception, not BaseException directly.

Key features

  • BaseException is the root of every exception; Exception is one of its direct subclasses
  • SystemExit, KeyboardInterrupt, GeneratorExit inherit BaseException but NOT Exception
  • except Exception: never catches those three — that separation is deliberate
  • ArithmeticError, LookupError, OSError are broad subclasses grouping related errors
  • A custom exception should inherit Exception, never BaseException directly

The core of the built-in hierarchy

The core of the built-in hierarchy
ClassParentCatches
BaseException(root)everything — never catch this directly
ExceptionBaseExceptionthe drawer for ordinary, handleable errors
SystemExitBaseExceptionraised by sys.exit() — not caught by except Exception
KeyboardInterruptBaseExceptionraised by Ctrl+C — not caught by except Exception
ArithmeticErrorExceptionparent of ZeroDivisionError, OverflowError
LookupErrorExceptionparent of KeyError, IndexError
OSErrorExceptionparent of FileNotFoundError, PermissionError, ConnectionError
The exception tree, top to bottom

BaseException

the true root — every exception inherits from here

Exception

the drawer for ordinary, handleable errors

ArithmeticError / LookupError / OSError

broad groups within Exception

ZeroDivisionError / KeyError / FileNotFoundError

the specific errors you actually catch

  1. BaseException — the true root — every exception inherits from here
  2. Exception — the drawer for ordinary, handleable errors
  3. ArithmeticError / LookupError / OSError — broad groups within Exception
  4. ZeroDivisionError / KeyError / FileNotFoundError — the specific errors you actually catch

Command

python
BaseException
 ├── SystemExit, KeyboardInterrupt, GeneratorExit   # NOT under Exception
 └── Exception                                       # catch this, not BaseException
      ├── ArithmeticError → ZeroDivisionError, OverflowError
      ├── LookupError     → KeyError, IndexError
      └── OSError         → FileNotFoundError, PermissionError, ConnectionError

except Exception excludes SystemExit/KeyboardInterrupt on purpose. Reach for BaseException only when you truly mean everything.

2.try / except

try wraps code that might raise an exception. except names the exception type to catch and recover from, running only when that type — or a subclass of it — is actually raised.

Key features

  • Code after the line that raised, still inside try, never runs
  • except ValueError: also catches subclasses of ValueError
  • A bare except: (no type) catches everything, including typos — avoid it
  • except Exception as e: binds the exception object to e for inspection
  • Multiple except blocks are tried top to bottom; the first match wins
try/except control flow

try: block runs

normal code, may raise

exception raised

remaining try lines are skipped

matching except runs

first except whose type matches

  1. try: block runs — normal code, may raise
  2. exception raised — remaining try lines are skipped
  3. matching except runs — first except whose type matches

Command

python
try:
    risky_call()
except ValueError as e:
    handle(e)   # runs only if risky_call() raises ValueError (or a subclass)

try wraps code that might fail; except names what it recovers from. Name a specific type — a bare except: hides bugs it was never meant to catch.

3.else clause

else on a try statement runs only if the try block raised nothing at all. It keeps success-path code out of try, so except only ever catches failures from the lines actually being protected.

Key features

  • else runs only when try completed with no exception raised
  • else runs before finally, if both are present
  • Code in else is NOT protected by the except blocks above it
  • Without else, success code placed at the end of try would be wrongly protected too
  • else is optional and rarely required, but keeps intent explicit

Command

python
try:
    value = int(text)
except ValueError:
    print("not a number")
else:
    print(f"parsed {value}")   # only runs if int(text) succeeded

else runs only when try raised nothing — it keeps success-only code from being accidentally caught by the except above it.

4.finally clause

finally always runs — on success, on a caught exception, on an uncaught exception, and even after a return inside try. It is the place for cleanup that must never be skipped, like closing a file or releasing a lock.

Key features

  • finally runs after try/except/else, no matter which path was taken
  • finally still runs even when the exception is not caught and propagates
  • finally runs even after a return statement inside try
  • A return inside finally silently discards a pending exception — avoid it
  • with statements handle most cleanup more cleanly than try/finally today

Command

python
try:
    risky_call()
finally:
    cleanup()   # always runs, even if risky_call() raised and was not caught

finally always runs on every exit path. Never put return inside it — that silently discards a pending exception.

5.raise

raise ExceptionType(message) stops normal execution and starts looking for a matching except, working outward through the call stack. A bare raise inside an except re-raises the exception currently being handled.

Key features

  • raise ValueError("message") both constructs and raises the exception in one line
  • A bare raise (no argument) inside except re-raises the exception being handled
  • raise stops the current function immediately — nothing after it in that scope runs
  • An uncaught raise propagates up through every calling function until something catches it
  • raise SomeError from None suppresses the "during handling of" context — see raise from

Command

python
def check_age(age):
    if age < 0:
        raise ValueError(f"age cannot be negative: {age}")
    return age

raise ExceptionType(message) stops execution and searches outward for a matching except. A bare raise re-raises the exception already being handled.

6.raise ... from ...

raise NewError("...") from original_error sets NewError.__cause__ to original_error and prints both tracebacks, labelled "the direct cause of." It replaces a low-level error with a more meaningful one without losing the original.

Key features

  • raise New(...) from original sets New.__cause__ = original explicitly
  • The traceback labels it "The above exception was the direct cause..."
  • raise New(...) from None suppresses the original — no chained traceback at all
  • Without `from`, Python still chains automatically via __context__ — see exception chaining
  • __cause__ is always explicit (via from); __context__ can be explicit or implicit

raise ... from ..., by what you pass

raise ... from ..., by what you pass
FormEffect
raise New(...) from originalsets __cause__ = original; traceback shows both, labelled "direct cause"
raise New(...) from Nonesets __suppress_context__ = True; traceback shows only the new exception
raise New(...) (no from)sets __context__ automatically if raised inside an except block
raise ... from ... links two exceptions explicitly

ValueError raised

the original, low-level failure

raise ConfigError(...) from e

sets __cause__ = e explicitly

both shown in traceback

"the direct cause of the following exception"

  1. ValueError raised — the original, low-level failure
  2. raise ConfigError(...) from e — sets __cause__ = e explicitly
  3. both shown in traceback — "the direct cause of the following exception"

Command

python
try:
    int(raw_value)
except ValueError as e:
    raise ConfigError("invalid config value") from e   # __cause__ = e

raise New(...) from original sets __cause__ and prints "the direct cause of" in the traceback. Use `from None` only when the original adds no value.

7.Exception chaining

If a new exception is raised while an except block is already handling one, Python automatically links them via __context__ — even with no `from` at all. The traceback shows both, labelled "during handling of."

Key features

  • __context__ is set automatically whenever raise happens inside an active except block
  • raise ... from ... additionally sets __cause__, which implies __context__ too
  • The implicit case is labelled "During handling of the above exception..."
  • The explicit `from` case is labelled "...was the direct cause of the following exception"
  • raise ... from None sets __suppress_context__ = True, hiding the chain entirely

__cause__ vs __context__

__cause__ vs __context__
AttributeSet byTraceback label
__cause__raise New(...) from original — explicit only"the direct cause of the following exception"
__context__any raise inside an active except block, with or without from"During handling of the above exception, another exception occurred"
__suppress_context__raise New(...) from Nonehides the chain — no second traceback shown

Command

python
try:
    step_one()
except SomeError:
    raise OtherError("cleanup also failed")   # no 'from' — __context__ still set

Raising inside an active except block sets __context__ automatically, even with no `from`. Use `from None` to suppress the chain.

8.Custom exceptions

A custom exception is a class that subclasses Exception, giving your code its own precise error type — InsufficientFundsError instead of a generic ValueError — that callers can catch specifically and attach structured data to.

Key features

  • A custom exception subclasses Exception, never BaseException directly
  • Naming convention: end the class name in "Error" or "Exception"
  • Override __init__ to store structured data and call super().__init__(message)
  • A custom exception's own attributes are readable on the caught instance
  • Group related custom exceptions under one shared base for a broad except

A minimal custom exception vs one carrying structured data

A minimal custom exception vs one carrying structured data
StyleWhen to use
class ConfigError(Exception): passthe type itself is the signal — no extra data needed
__init__ storing extra attributescallers need structured data, not just a message string
A shared base for related errorslets one except catch the whole family when needed
A custom exception, end to end

class InsufficientFundsError(Exception)

subclasses Exception

raise InsufficientFundsError(balance, amount)

carries structured data

except InsufficientFundsError as e

e.balance, e.amount readable

  1. class InsufficientFundsError(Exception) — subclasses Exception
  2. raise InsufficientFundsError(balance, amount) — carries structured data
  3. except InsufficientFundsError as e — e.balance, e.amount readable

Command

python
class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"cannot withdraw {amount}, balance is {balance}")

Subclass Exception, call super().__init__(message), and store extra data as attributes for callers to read.

9.Multiple exception types

except (TypeError, ValueError): catches either type with one handler, using the same recovery code for both. Stack separate except blocks instead when each type needs a different response.

Key features

  • except (TypeError, ValueError): needs the parentheses — a tuple, not two names
  • Matches if the raised exception is an instance of ANY type in the tuple
  • Use one shared except when the recovery code is identical either way
  • Stack separate except blocks when each type needs a different response
  • The first except clause (top to bottom) that matches is the one that runs

Command

python
try:
    value = int(raw)
except (TypeError, ValueError) as e:
    print(f"bad input: {e}")   # same handling for either type

except (TypeA, TypeB): catches either with one shared handler — parentheses make it a tuple. Split into separate blocks when each type needs its own response.

Remember

  • except Exception is the right broad catch — it excludes SystemExit and KeyboardInterrupt on purpose.
  • finally always runs, on every exit path — never put return inside it, it silently discards a pending exception.
  • raise New(...) from original sets __cause__ explicitly; a raise inside except sets __context__ even with no from at all.
  • except (TypeA, TypeB): needs the parentheses — a tuple, not two names.

Pro tip

  • Name a specific exception type rather than a bare except: — a bare except also catches Ctrl+C and sys.exit().
  • else on try runs only when nothing raised — it keeps success-path code from being wrongly caught by except.
  • Always call super().__init__(message) in a custom exception, or str(e) comes back empty.
  • Use raise ... from None only when the original exception truly adds no value to the reader.

Next up

  • Production error handling

Sources

  • Errors and Exceptions — docs.python.org/3/tutorial/errors.html
  • Built-in Exceptions — docs.python.org/3/library/exceptions.html
  • Every example executed on Python 3.14.3 · checked 2026-08-21

Spotted a mistake on this page? Tell us.