04open 25 min

Error handling: exceptions and None without trying to recreate Result everywhere

Translate Result, Option, and explicit absence into Python's exceptions and None-based contracts.

by the end of this lesson you can

  • Separates ordinary absence from actual failure
  • Uses exceptions only for real failure cases
  • Does not try to recreate Result mechanically

Overview

Rust developers already think carefully about failure and absence, but Python expresses them differently. Exceptions represent exceptional failure, and None is often used for ordinary absence. The skill is learning when each contract is appropriate instead of searching for one direct replacement for Result or Option.

In Rust, you often

use Result for failure, Option for absence, and rely on the type system to keep those contracts separate.

In Python, the common pattern is

to raise exceptions for failure, return None when absence is ordinary, and keep the distinction clear through naming, docs, tests, and call-site expectations.

why this difference matters

This is one of the biggest conceptual shifts because Python keeps the contracts real, but encodes less of them in the type system itself.

Rust

let user = lookup_user(id).ok_or(UserError::Missing)?;

Python

user = lookup_user(user_id)
if user is None:
    raise UserNotFoundError(user_id)

Deeper comparison

Rust version

fn first_admin(users: &[User]) -> Option<&User> {
    users.iter().find(|user| user.admin)
}

Python version

def first_admin(users):
    for user in users:
        if user.is_admin:
            return user
    return None

Reflect

Why is it important not to treat None and exceptions as one-to-one replacements for Option and Result?

what a strong answer notices

A strong answer mentions different language conventions, clearer API design at boundaries, and the need to decide whether absence is ordinary or exceptional instead of assuming the type system will encode it for you.

Rewrite

Rewrite this Rust function into Python and choose between returning None and raising an exception intentionally.

Rewrite this Rust

fn load_user(id: UserId) -> Result<Option<User>, LoadError> {
    todo!()
}

what good looks like

  • Separates ordinary absence from actual failure
  • Uses exceptions only for real failure cases
  • Does not try to recreate Result mechanically

Practice

Design a Python config loader that distinguishes parse failure from an optional setting that is simply missing.

success criteria

  • Uses exceptions for true failure
  • Uses None or a default only where absence is normal
  • Explains how the caller can read the contract clearly

Common mistakes

  • Treating None as a sloppy catch-all instead of a specific contract.
  • Building homemade Result containers for ordinary Python application code too early.
  • Catching broad exceptions because the Rust version handled every error path explicitly in the type system.

takeaways

  • This is one of the biggest conceptual shifts because Python keeps the contracts real, but encodes less of them in the type system itself.
  • A strong answer mentions different language conventions, clearer API design at boundaries, and the need to decide whether absence is ordinary or exceptional instead of assuming the type system will encode it for you.
  • Uses exceptions for true failure