A modern, type-safe Railway Oriented Programming (ROP) library for Python that provides a robust way to handle errors and compose operations.
Metropy implements the Railway Oriented Programming pattern in Python, offering a more functional approach to error handling compared to traditional try/except blocks. It helps you write more maintainable and composable code by treating success and failure cases as first-class citizens.
The library is designed with Python's type system in mind, providing full type safety and IDE support while maintaining a clean, pythonic API.
- 🚂 Railway Oriented Programming: Elegant error handling with the
Result[T, E]type - 🔒 Type Safety: Full type hints support with generics and modern Python typing
- 🔗 Functional Composition: Chain operations with the
bindandmapoperators - 🎯 Zero Dependencies: Core functionality requires only Python 3.9+
- 🔄 Async Support: First-class support for async/await operations
- 🛡️ Pydantic Integration: Seamless integration with Pydantic for data validation
- 🎨 Pythonic API: Clean, intuitive interface following Python's best practices
- 🔍 Comprehensive Error Handling: Rich utilities for error transformation and recovery
- 📚 Rich Documentation: Comprehensive documentation with examples and API reference
# Using pip with UV (recommended)
uv pip install metropy-rop
# Using pip
pip install metropy-rop
# With Pydantic integration
pip install "metropy-rop[pydantic]"
# With documentation tools
pip install "metropy-rop[docs]"from metropy import Result, railway
@railway
def divide(a: float, b: float) -> Result[float, str]:
if b == 0:
return Result.failure("Division by zero")
return Result.success(a / b)
# Simple usage
result = divide(10, 2)
print(result.unwrap_or(0)) # Output: 5.0
# Error handling
result = divide(10, 0)
print(result.unwrap_error()) # Output: "Division by zero"from metropy import Result, bind, map
def validate_input(x: int) -> Result[int, str]:
return (Result.success(x)
if x > 0
else Result.failure("Input must be positive"))
def double(x: int) -> int:
return x * 2
def process_data(x: int) -> Result[str, str]:
return Result.success(f"Result: {x}")
# Compose functions using the | operator
pipeline = (
validate_input
| map(double)
| bind(process_data)
)
result = pipeline(5)
print(result.unwrap()) # Output: "Result: 10"from metropy import async_railway
from aiohttp import ClientSession
@async_railway
async def fetch_data(url: str) -> Result[dict, str]:
async with ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
return Result.success(await response.json())
return Result.failure(f"HTTP {response.status}")
# Usage with async/await
async def main():
result = await fetch_data("https://api.example.com/data")
if result.is_success():
print(f"Data: {result.unwrap()}")
else:
print(f"Error: {result.unwrap_error()}")from pydantic import BaseModel
from metropy.pydantic import validate_model
class User(BaseModel):
name: str
age: int
@validate_model(User)
def create_user(data: dict) -> Result[User, str]:
# Validation is handled automatically by the decorator
return Result.success(data)
# Usage
result = create_user({"name": "John", "age": 30})
print(result.unwrap().name) # Output: "John"
# Invalid data
result = create_user({"name": "John"}) # Missing age
print(result.is_failure()) # Output: True- Error Recovery: Use
recoverto provide fallback values - Error Transformation: Transform errors with
map_error - Parallel Validation: Combine multiple results with
all_successful - Debugging: Use
tapfor logging and debugging - Custom Error Types: Define your own error types with full type safety
- Clone the repository
- Install development dependencies:
uv pip install -e ".[dev,docs]" - Run tests:
pytest
- Run type checks:
mypy src
- Run linting:
ruff check src
- Build documentation:
# Serve documentation locally mkdocs serve # Build documentation mkdocs build
We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.
This library is inspired by:
- Railway Oriented Programming pattern
- Rust's
Resulttype - Haskell's
Eithermonad - F#'s Railway Oriented Programming