04open 25 min

Error handling: exceptions, custom exception types, and when to let errors bubble

Translate from returned error values into Python's exception style without making the control flow noisy.

by the end of this lesson you can

  • Uses exceptions instead of returned error values
  • Keeps the happy path readable
  • Only adds try/except where recovery or translation is useful

Overview

Go developers are used to visible error paths in ordinary control flow. Python handles failure differently: exceptions carry the error path out of the immediate line-by-line flow, and callers catch them where recovery actually makes sense. The goal is not to hide failure. It is to keep failure handling attached to the right boundary.

In Go, you often

return an error value explicitly and check it immediately at each step.

In Python, the common pattern is

to raise exceptions, define custom exception types when they clarify intent, and catch errors at boundaries that can recover or translate them meaningfully.

why this difference matters

This lesson matters because many Go developers either over-catch in Python or try to simulate returned errors everywhere instead of learning Python's normal failure model.

Go

user, err := loadUser(id)
if err != nil {
    return err
}

Python

user = load_user(user_id)

Deeper comparison

Go version

cfg, err := loadConfig(path)
if err != nil {
    return fmt.Errorf("load config: %w", err)
}
return run(cfg)

Python version

try:
    cfg = load_config(path)
    run(cfg)
except ConfigError as error:
    raise RuntimeError("load config failed") from error

Reflect

When is it better in Python to let an exception bubble instead of catching it immediately the way you might handle an error in Go?

what a strong answer notices

A strong answer mentions recovery boundaries, avoiding repetitive local handling, and catching exceptions where the program can add context or decide what to do next.

Rewrite

Rewrite this Go function into Python using exceptions and only catching where it helps.

Rewrite this Go

func saveUser(user User) error {
    if err := validate(user); err != nil {
        return err
    }
    if err := repo.Save(user); err != nil {
        return err
    }
    return nil
}

what good looks like

  • Uses exceptions instead of returned error values
  • Keeps the happy path readable
  • Only adds try/except where recovery or translation is useful

Practice

Design a Python API client boundary that raises custom exceptions for request failure and validation failure, and explain where callers should catch them.

success criteria

  • Uses exception types intentionally
  • Separates error creation from recovery boundaries
  • Avoids recreating Go-style error plumbing line by line

Common mistakes

  • Catching exceptions too early because Go trained immediate error handling reflexes.
  • Using broad except blocks where specific exception types would be clearer.
  • Trying to model Python failures as returned status values instead of exceptions.

takeaways

  • This lesson matters because many Go developers either over-catch in Python or try to simulate returned errors everywhere instead of learning Python's normal failure model.
  • A strong answer mentions recovery boundaries, avoiding repetitive local handling, and catching exceptions where the program can add context or decide what to do next.
  • Uses exception types intentionally