Functional Error Handling in Python: A Guide to the Returns Package
Overview
Python's traditional error handling relies heavily on exceptions and the None value. While standard, this approach often creates brittle code where it's unclear what errors a function might throw. The try-except blocks.
Prerequisites
To follow this tutorial, you should have a solid grasp of
Key Libraries & Tools
- returns: The primary library for functional error handling, providing
Maybe,Result, andIOcontainers. - Type Annotations: Essential for getting the most out of returns, though some IDEs likePylancemay require specific plugins to handle these complex types.
Code Walkthrough
Handling Missing Data with Maybe
The Maybe container represents a value that might not exist. It has two states: Some (the value exists) and Nothing (the value is missing).

from returns.maybe import Maybe, Some, Nothing
def find_user(user_id: int) -> Maybe[User]:
user = users_db.get(user_id)
return Some(user) if user else Nothing
# Usage with mapping
user_name = find_user(1).map(lambda user: user.name)
In this snippet, find_user doesn't return None. It returns a container. If the user is missing, calling .map() simply does nothing, preventing the dreaded AttributeError: 'NoneType' object has no attribute 'name'.
Railway-Oriented Programming with Result
The Result container handles operations that can fail, using Success and Failure tracks.
from returns.result import Result, Success, Failure
def divide(a: int, b: int) -> Result[int, str]:
if b == 0:
return Failure("Division by zero")
return Success(a // b)
# Chaining operations
result = divide(10, 2).bind(lambda x: Success(x + 10))
By using .bind(), the next function only executes if the previous step was a Success. If any step fails, the Failure state propagates through the chain automatically.
Managing Side Effects with IO
The IO container isolates side effects like file reading or network requests from pure logic.
from returns.io import IOResult, IOSuccess, IOFailure
def read_file(path: str) -> IOResult[str, Exception]:
try:
with open(path, 'r') as f:
return IOSuccess(f.read())
except IOError as e:
return IOFailure(e)
This encapsulates the "impurity" of hitting the disk, forcing the developer to be explicit about how they handle the data once it's retrieved.
Syntax Notes
- The @safe Decorator: Use
@safeto automatically wrap functions that raise exceptions into aResultcontainer. It catches errors and sends them to theFailuretrack. - Structural Pattern Matching: Python's
matchstatement is the cleanest way to extract values from containers:match result: case Success(value): ....
Practical Examples
This approach is ideal for complex data pipelines or SDK development where you want to ensure cross-language compatibility with functional languages like
Tips & Gotchas
Avoid overusing the @safe decorator on every function, as it adds unnecessary boilerplate. Be aware that

Fancy watching it?
Watch the full video and context