Skip to content

Error Handling#

Production-quality Python distinguishes expected outcomes from exceptional failures, designs exception hierarchies deliberately, and cleans up resources reliably. This page goes beyond Basic Error Handling with chaining, context utilities, and patterns for robust code.

How to use this page

Know when to raise vs return. Use with File Handling for I/O errors and Functions and Scope for contract design.

At a glance
Track Python Intermediate
Sections 10 major topics
Outline Use the right-hand TOC to jump

Topics: Exception hierarchy (detailed) · Designing exception types · try / except / else / finally — full semantics · Exception chaining · Exception groups (3.11+) · EAFP vs LBYL — intermediate guidance · contextlib utilities · Logging vs raising · … (+2 more)

  1. Exception hierarchy (detailed)
  2. Designing exception types
  3. try / except / else / finally — full semantics
  4. Exception chaining
  5. Exception groups (3.11+)
  6. EAFP vs LBYL — intermediate guidance
  7. contextlib utilities
  8. Logging vs raising
  9. Assertions vs exceptions
  10. Interview vs production patterns

Exception hierarchy (detailed)#

BaseException
├── SystemExit              # sys.exit()
├── KeyboardInterrupt       # Ctrl+C
├── GeneratorExit
└── Exception               # Catch this, not BaseException
    ├── StopIteration
    ├── StopAsyncIteration
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   ├── OverflowError
    │   └── FloatingPointError
    ├── AssertionError
    ├── AttributeError
    ├── BufferError
    ├── EOFError
    ├── ImportError
    │   └── ModuleNotFoundError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── MemoryError
    ├── NameError
    │   └── UnboundLocalError
    ├── OSError                   # I/O, OS failures
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   ├── TimeoutError
    │   └── ...
    ├── ReferenceError
    ├── RuntimeError
    │   ├── NotImplementedError
    │   └── RecursionError
    ├── SyntaxError
    ├── SystemError
    ├── TypeError
    └── ValueError

Catch Exception, never bare BaseException — you do not want to catch KeyboardInterrupt or SystemExit accidentally.

try:
    risky()
except Exception as e:
    log.exception("failed")   # includes traceback
    raise                     # re-raise after logging

Designing exception types#

Simple domain exception#

class AppError(Exception):
    """Base for all application errors."""

class ValidationError(AppError):
    """User input failed validation."""

class NotFoundError(AppError):
    def __init__(self, resource: str, id: str):
        self.resource = resource
        self.id = id
        super().__init__(f"{resource} {id!r} not found")

Guidelines:

  • Inherit from Exception (or your app base).
  • Put structured data on the exception object (resource, id).
  • Keep messages actionable for humans.
  • Don't over-granularize — 5–10 types per project is often enough.

When to subclass ValueError vs Exception#

Inherit from When
ValueError Bad value for otherwise valid API (like built-ins)
AppError Domain-specific failures
RuntimeError Internal inconsistency / bug-like state

try / except / else / finally — full semantics#

def process_file(path: str) -> dict:
    f = None
    try:
        f = open(path)
        data = parse(f.read())
    except FileNotFoundError:
        return {}
    except UnicodeDecodeError as e:
        raise ValueError(f"not valid UTF-8: {path}") from e
    else:
        audit.log(f"processed {path}")
    finally:
        if f is not None:
            f.close()
    return data
Clause Runs when
try Always first
except Matching exception raised in try
else try completed without exception
finally Always — before return/break propagates

finally and return#

def demo():
    try:
        return 1
    finally:
        print("cleanup")   # runs before return value delivered

If finally also executes return, it overrides the try/except return (avoid this).

Exception in except or finally#

If except raises a new exception, original is lost unless chained. If finally raises, it may mask prior exception — keep finally minimal (cleanup only).


Exception chaining#

try:
    config = json.loads(raw)
except json.JSONDecodeError as e:
    raise ConfigurationError("invalid JSON config") from e
Form Effect
raise X from e X.__cause__ = e — show both in traceback
raise X from None Suppress context display
bare raise Re-raise current exception with original traceback

Use chaining when wrapping low-level errors (I/O, parse) into domain errors.


Exception groups (3.11+)#

Parallel failures in structured concurrency:

try:
    raise ExceptionGroup("failures", [
        ValueError("bad value"),
        TypeError("bad type"),
    ])
except* ValueError as eg:
    handle_value_errors(eg.exceptions)
except* TypeError as eg:
    handle_type_errors(eg.exceptions)

Relevant for asyncio.TaskGroup and batch validation — less common in DSA interviews.


EAFP vs LBYL — intermediate guidance#

Style Pythonic when Example
EAFP Happy path common; failure exceptional try: d[k] / except KeyError
LBYL Failure common; check cheap if key in d
.get() Missing key with default d.get(k, 0)
# EAFP — file exists and readable
try:
    with open(path) as f:
        return f.read()
except FileNotFoundError:
    return default_content

# LBYL — avoid exception in tight loop with frequent misses
if key in cache:
    return cache[key]

For dict counting, .get() is clearest. For "key must exist or crash", use d[k] directly.


contextlib utilities#

suppress — ignore specific exceptions#

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove(temp_path)

Use sparingly — silent failure hides bugs.

contextmanager — write your own with#

from contextlib import contextmanager

@contextmanager
def temporary_setting(name, value):
    old = getattr(config, name)
    setattr(config, name, value)
    try:
        yield
    finally:
        setattr(config, name, old)

ExitStack — dynamic number of context managers#

from contextlib import ExitStack

with ExitStack() as stack:
    files = [stack.enter_context(open(p)) for p in paths]
    process_all(files)
# all files closed

Logging vs raising#

Situation Action
Caller can recover Raise specific exception
Cannot continue Raise — don't return error codes
Record and continue Log warning, handle locally
Unexpected bug Log exception (exc_info=True), re-raise or crash
import logging
log = logging.getLogger(__name__)

try:
    result = compute()
except ValueError as e:
    log.warning("invalid input: %s", e)
    return fallback
except Exception:
    log.exception("unexpected failure")
    raise

Never except Exception: pass in production.


Assertions vs exceptions#

assert condition, "message"   # disabled with python -O
Use Tool
Internal invariants / debug assert
User input validation raise ValueError
API contract violations Custom exception

Interview vs production patterns#

Algorithm problems — prefer sentinels#

def find(nums: list[int], target: int) -> int:
    for i, x in enumerate(nums):
        if x == target:
            return i
    return -1

API / parsing — raise on bad input#

def parse_port(s: str) -> int:
    try:
        port = int(s)
    except ValueError as e:
        raise ValueError(f"invalid port: {s!r}") from e
    if not 1 <= port <= 65535:
        raise ValueError(f"port out of range: {port}")
    return port

Multiple error collection#

errors: list[str] = []
if not name:
    errors.append("name required")
if age < 0:
    errors.append("age must be non-negative")
if errors:
    raise ValidationError("; ".join(errors))

Interview traps (quick reference)#

Trap What goes wrong Safe approach
Bare except: Catches KeyboardInterrupt Catch Exception or specific
except Exception: pass Silent failure Log and handle
Assert for user input Disabled with -O Raise ValueError
Broad catch then re-wrap poorly Lost context raise ... from e
Heavy logic in finally Masks exceptions Cleanup only
Using exceptions for flow control in hot loops Slow .get(), membership test

Mental model checklist#

  1. What is the difference between else and finally in try/except?
  2. When should you use raise ... from e?
  3. Why not catch BaseException?
  4. When is EAFP better than LBYL for your case?
  5. What happens if finally raises an exception?

What's next#

Topic Page
Basic exceptions Basic Error Handling
File I/O errors File Handling
Unit testing errors Unit Testing
Logging Data Handling and Standard Library