All built-in exceptions in Python form a class hierarchy rooted at BaseException. Most application-level exceptions inherit from Exception, a direct subclass of BaseException. When you catch an exception class, you also catch every class that inherits from it.
The family tree of errors
What you will learn: how all Python exceptions form one inheritance tree under BaseException, the dozen exceptions you meet most, and why catching a parent catches all its children.
How to read this tab: Study the tree diagram first — once you see the shape, catching the right level becomes obvious.
Every error in Python is an object built from a class, and those classes are arranged in a family tree. When you catch a parent class, you automatically catch all its children. Knowing the tree means you catch exactly what you intend.
Catch Exception, not BaseException. KeyboardInterrupt (Ctrl-C) and SystemExit sit beside Exception, not under it, precisely so that except Exception: lets the user stop your program. A bare except: catches those too — almost always a bug.
The shape of the tree
At the very top sits BaseException. Almost everything you handle inherits from Exception, which is one level down. A few special exceptions — the ones that should usually not be caught — sit beside Exception rather than under it.
BaseException # the root — catches absolutely everything
├── SystemExit # raised by sys.exit() — let it propagate
├── KeyboardInterrupt # raised by Ctrl-C — let it propagate
├── GeneratorExit
└── Exception # ← catch THIS or its children, not BaseException
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError # list[999]
│ └── KeyError # dict["missing"]
├── TypeError # "5" + 3
├── ValueError # int("abc")
├── OSError
│ ├── FileNotFoundError
│ └── PermissionError
├── AttributeError # obj.missing_attribute
├── NameError # using an undefined variable
└── ... many moreexcept Exception: catches ordinary errors but deliberately lets KeyboardInterrupt (Ctrl-C) and SystemExit through — so the user can always stop your program. except BaseException: (or a bare except:) swallows even those, which is almost always a bug.
The name tells you the category. ValueError = right type but wrong value (int("abc")). TypeError = wrong type entirely ("5"+3). LookupError = asked for something absent (IndexError for lists, KeyError for dicts). Reading the exception name usually tells you what went wrong before you read the message.
The exceptions you will meet most
int("abc") # ValueError: invalid literal for int()
"5" + 3 # TypeError: can only concatenate str to str
[1, 2, 3][99] # IndexError: list index out of range
{"a": 1}["b"] # KeyError: 'b'
open("nope.txt") # FileNotFoundError
undefined_name # NameError: name 'undefined_name' is not defined
"hi".missing() # AttributeError: 'str' object has no attribute 'missing'
10 / 0 # ZeroDivisionError: division by zero
# Each name tells you the category of problem:
# ValueError = right type, wrong value
# TypeError = wrong type entirely
# LookupError = asked for something not there (IndexError/KeyError)✅ Beginner tab complete
- I know all exceptions inherit from BaseException, and most from Exception
- I know to catch Exception, never BaseException or bare except
- I can name what ValueError, TypeError, KeyError, IndexError each mean
- I know catching a parent class catches all its child classes
Catching by level and custom exceptions
What you will learn: catching specific vs broad (and why order matters), defining custom exception classes, and exception chaining with raise...from.
How to read this tab: Practice ordering except clauses most-specific-first — reversing them is a common silent bug.
Order except clauses most-specific-first. Python uses the first matching clause. If except OSError comes before except FileNotFoundError, the specific handler never runs because the parent already matched. Always put children above parents.
Catching by level — specific vs broad
Because catching a parent catches all children, you choose how broadly to catch by choosing how high in the tree you reach. Catch as specifically as you can.
# Catching a parent catches all children
try:
data = {"a": 1}
print(data["b"])
except LookupError: # catches BOTH KeyError and IndexError
print("key or index missing")
# Catch multiple specific types in one except
try:
value = int(user_input)
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Bad input: {e}")
# Order matters — most specific FIRST
try:
risky()
except FileNotFoundError: # specific child first
print("file missing")
except OSError: # broader parent second
print("some OS error")
# If reversed, OSError would catch FileNotFoundError and the
# specific handler would never run.
# Inspect the exception object
try:
int("abc")
except ValueError as e:
print(type(e).__name__) # 'ValueError'
print(str(e)) # the message
print(e.args) # ('invalid literal...',)raise X from e keeps the story. When you catch one exception and raise another, from e links them so the traceback shows both — the original cause and your domain exception. Without it, debugging gets much harder.
Custom exceptions and chaining
# Define a custom exception by inheriting from Exception
class ValidationError(Exception):
"""Raised when user input fails validation."""
pass
class AgeError(ValidationError): # a more specific subclass
pass
def set_age(age):
if not isinstance(age, int):
raise AgeError(f"Age must be an integer, got {type(age).__name__}")
if age < 0:
raise AgeError("Age cannot be negative")
return age
# Callers can catch the specific type OR the parent
try:
set_age(-5)
except ValidationError as e: # catches AgeError too (it's a subclass)
print(f"Validation failed: {e}")
# Exception chaining — preserve the original cause
def load_config(path):
try:
with open(path) as f:
return parse(f.read())
except FileNotFoundError as e:
raise ValidationError("Config missing") from e
# 'from e' records the original — the traceback shows BOTHexcept Exception first, it catches everything and your specific handlers below never run.from e links the new exception to the original cause, so the traceback shows the full story. Without it, the original context can be lost or shown as "During handling of the above exception, another occurred."✅ Intermediate tab complete
- I put specific exceptions before general ones in except clauses
- I can catch multiple types with except (A, B)
- I can define a custom exception by inheriting from Exception
- I use raise X from e to preserve the original cause
Exception groups, except*, and chaining attributes
What you will learn: PEP 3151 OSError consolidation, PEP 654 ExceptionGroup and except*, and the __cause__/__context__ chaining attributes.
How to read this tab: Read when working with concurrent code or building libraries with rich error reporting.
BaseExceptionGroup, except*, and the OSError consolidation
Two structural changes shaped the modern hierarchy. PEP 3151 (Python 3.3) collapsed the old tangle of IOError, OSError, WindowsError, and socket.error into a single OSError tree with meaningful subclasses like FileNotFoundError and PermissionError — so you can catch FileNotFoundError directly instead of inspecting errno. PEP 654 (Python 3.11) added ExceptionGroup and the except* syntax for handling multiple simultaneous exceptions, which arise in concurrent code where several tasks may fail at once.
# PEP 654 — ExceptionGroup and except* (Python 3.11+)
def run_tasks():
errors = []
for task in tasks:
try:
task.run()
except Exception as e:
errors.append(e)
if errors:
raise ExceptionGroup("task failures", errors)
# except* handles groups — each clause peels off matching exceptions
try:
run_tasks()
except* ValueError as eg:
print(f"{len(eg.exceptions)} value errors")
except* TypeError as eg:
print(f"{len(eg.exceptions)} type errors")
# Both clauses can run — a group may contain both kinds
# __cause__ vs __context__ — the two chaining attributes
try:
try:
1 / 0
except ZeroDivisionError as e:
raise ValueError("bad") from e
except ValueError as e:
print(e.__cause__) # the ZeroDivisionError (explicit, via 'from')
print(e.__context__) # also set (implicit — what we were handling)
print(e.__suppress_context__) # True when 'from' was usedEvery raised exception records two chaining attributes: __context__ is set implicitly to whatever exception was being handled when the new one was raised, and __cause__ is set explicitly by raise ... from .... The from None form sets __suppress_context__ to hide the implicit chain entirely — useful when re-raising a clean domain exception and you do not want the internal cause cluttering the traceback.
✅ Expert tab complete
- I know ExceptionGroup and except* handle multiple simultaneous errors (3.11+)
- I know __cause__ is set by raise...from and __context__ is set implicitly
- I know FileNotFoundError/PermissionError are OSError subclasses (PEP 3151)