Python Result Type
A functional programming Result type for Python, inspired by Rust's Result<T, E> and similar to PyMonad's Either, but with more intuitive naming (Success/Failure instead of Right/Left).
🚀 Features
- Intuitive API:
SuccessandFailureinstead of crypticRight/Left - Rust-like Aliases: Optional
OkandErraliases for Rust developers - Type Safe: Full generic type support with
Result[T, E] - Chainable Operations: Use
.then()method or>>operator for clean chaining - Exception Safety: Automatic exception handling in chained operations
- Async/Await Support: Full async support with
AsyncResultwrapper - Zero Dependencies: Pure Python with no external dependencies
- Comprehensive: Includes helper functions and decorators for common patterns
- Well Tested: 100% test coverage with extensive edge case testing
📦 Installation
pip install python-result-type
🎯 Quick Start
Basic Usage
from result_type import Success, Failure, Result
def divide(a: float, b: float) -> Result[float, str]:
if b == 0:
return Failure("Division by zero")
return Success(a / b)
# Success case
result = divide(10, 2)
if result.is_success():
print(f"Result: {result.value}") # Result: 5.0
else:
print(f"Error: {result.error}")
# Failure case
result = divide(10, 0)
if result.is_failure():
print(f"Error: {result.error}") # Error: Division by zero
Chaining Operations
Chain operations that can fail using .then() or the >> operator:
from result_type import Success, Failure
def divide(a: float, b: float) -> Result[float, str]:
if b == 0:
return Failure("Division by zero")
return Success(a / b)
def multiply_by_2(x: float) -> Result[float, str]:
return Success(x * 2)
def subtract_1(x: float) -> Result[float, str]:
if x < 1:
return Failure("Result would be negative")
return Success(x - 1)
# Method 1: Using .then() method
result = (
divide(10, 2)
.then(multiply_by_2)
.then(subtract_1)
.map(lambda x: x + 5)
)
# Method 2: Using >> operator (cleaner syntax)
result = divide(10, 2) >> multiply_by_2 >> subtract_1
# Method 3: Mixed approach
result = (
divide(10, 2)
>> multiply_by_2
.map(lambda x: x + 10) # Transform without failure
>> subtract_1
)
if result.is_success():
print(f"Final result: {result.value}")
else:
print(f"Error occurred: {result.error}")
Safe Function Calls
Automatically handle exceptions with safe_call:
from result_type import safe_call
# Wrap risky function calls
result = safe_call(
lambda: 10 / 0,
"Math operation failed"
)
if result.is_failure():
print(result.error) # "Math operation failed: division by zero"
# Use as decorator
from result_type import safe_call_decorator
@safe_call_decorator("Database error")
def risky_database_operation():
# Some operation that might throw
return fetch_user_from_db()
result = risky_database_operation()
Rust-like Aliases (Ok/Err)
For developers familiar with Rust's Result<T, E>, this library provides Ok and Err aliases for Success and Failure:
from result_type import Ok, Err, ok, err
def divide_rust_style(a: float, b: float):
if b == 0:
return Err("Division by zero")
return Ok(a / b)
# Usage is identical to Success/Failure
result = divide_rust_style(10, 2)
if result.is_success():
print(f"Result: {result.value}") # Result: 5.0
# Chain operations using Rust-style
def multiply_by_2(x):
return Ok(x * 2)
def validate_positive(x):
if x > 0:
return Ok(x)
return Err("Must be positive")
result = Ok(5) >> multiply_by_2 >> validate_positive
if result.is_success():
print(f"Final: {result.value}") # Final: 10
# Helper functions with Rust naming
success_result = ok(42) # Same as Success(42)
error_result = err("oops") # Same as Failure("oops")
# Mix and match - they're the same types!
mixed_result = Ok(10) >> (lambda x: Success(x * 2)) # Works perfectly
print(Ok(42) == Success(42)) # True
📚 Complete API Reference
Core Types
Result[T, E]
Abstract base class representing either success or failure.
Methods:
is_success() -> bool- Check if result is Successis_failure() -> bool- Check if result is Failurethen(func: Callable[[T], Result[U, E]]) -> Result[U, E]- Chain operationsmap(func: Callable[[T], U]) -> Result[U, E]- Transform success valuemap_error(func: Callable[[E], F]) -> Result[T, F]- Transform error valueunwrap() -> T- Extract value or raise exceptionunwrap_or(default: T) -> T- Extract value or return defaultunwrap_or_else(func: Callable[[E], T]) -> T- Extract value or compute from error
Success[T]
Represents successful result containing a value.
success_result = Success(42)
print(success_result.value) # 42
print(success_result.is_success()) # True
Failure[E]
Represents failed result containing an error.
failure_result = Failure("Something went wrong")
print(failure_result.error) # "Something went wrong"
print(failure_result.is_failure()) # True
Rust-like Aliases
Ok and Err
Type aliases for Success and Failure respectively.
from result_type import Ok, Err
# These are identical to Success/Failure
ok_result = Ok(42) # Same as Success(42)
err_result = Err("error") # Same as Failure("error")
ok(value: T) -> Success[T] and err(error: E) -> Failure[E]
Helper functions with Rust-style naming.
from result_type import ok, err
result = ok(42) # Same as success(42) or Success(42)
error = err("oops") # Same as failure("oops") or Failure("oops")
Helper Functions
success(value: T) -> Success[T]
Create a Success result.
from result_type import success
result = success(42) # Same as Success(42)
failure(error: E) -> Failure[E]
Create a Failure result.
from result_type import failure
result = failure("error") # Same as Failure("error")
safe_call(func: Callable[[], T], error_msg: str = None) -> Result[T, str]
Safely call a function that might raise exceptions.
from result_type import safe_call
result = safe_call(lambda: risky_operation())
if result.is_failure():
print(f"Operation failed: {result.error}")
safe_call_decorator(error_msg: str = None)
Decorator version of safe_call.
from result_type import safe_call_decorator
@safe_call_decorator("API call failed")
def call_external_api():
return requests.get("https://api.example.com").json()
result = call_external_api() # Returns Result[dict, str]
🔄 Chaining Operations
Error Propagation
When chaining operations, errors automatically propagate:
result = (
Success(10)
>> (lambda x: Failure("Something went wrong")) # This fails
>> (lambda x: Success(x * 2)) # This won't execute
>> (lambda x: Success(x + 1)) # Neither will this
)
print(result.error) # "Something went wrong"
Exception Handling in Chains
Exceptions in chained operations are automatically converted to Failure:
def risky_operation(x: int) -> Result[int, str]:
return Success(x / 0) # This will raise ZeroDivisionError
result = Success(10) >> risky_operation
print(result.is_failure()) # True
print(type(result.error)) # <class 'ZeroDivisionError'>
🔄 Async/Await Support
The library includes full async/await support for modern Python applications with the AsyncResult wrapper:
Basic Async Usage
import asyncio
from result_type import Success, Failure, Result
from result_type.async_result import AsyncResult, async_safe_call
async def fetch_user(user_id: int) -> Result[dict, str]:
await asyncio.sleep(0.1) # Simulate API call
if user_id <= 0:
return Failure("Invalid user ID")
return Success({"id": user_id, "name": f"User {user_id}"})
# Use async operations
result = await fetch_user(123)
if result.is_success():
print(f"Found user: {result.value}")
Async Chaining
Chain async and sync operations seamlessly:
async def fetch_user_posts(user: dict) -> Result[list, str]:
await asyncio.sleep(0.1)
return Success([f"Post {i}" for i in range(3)])
def format_summary(posts: list) -> Result[str, str]:
return Success(f"User has {len(posts)} posts")
# Chain async and sync operations
pipeline = (AsyncResult(fetch_user(123))
.then_async(fetch_user_posts) # Async operation
.then_sync(format_summary)) # Sync operation
result = await pipeline.resolve()
if result.is_success():
print(result.value) # "User has 3 posts"
Async Safe Calls
Handle async exceptions safely:
from result_type.async_result import async_safe_call, async_safe_call_decorator
# Function approach
async def risky_api_call():
# Might raise an exception
return await some_external_api()
result = await async_safe_call(risky_api_call, "API Error")
# Decorator approach
@async_safe_call_decorator("Database Error")
async def database_operation():
return await db.fetch_data()
result = await database_operation() # Returns Result[Any, str]
Gathering Multiple Async Results
Process multiple async operations concurrently:
from result_type.async_result import gather_results
async def fetch_data(source: str) -> Result[str, str]:
await asyncio.sleep(0.1)
return Success(f"Data from {source}")
# Gather results - stops at first failure
async_operations = [
AsyncResult(fetch_data("source1")),
AsyncResult(fetch_data("source2")),
AsyncResult(fetch_data("source3")),
]
combined = await gather_results(*async_operations)
if combined.is_success():
print(combined.value) # ["Data from source1", "Data from source2", "Data from source3"]
Converting Regular Awaitables
Convert any awaitable to an AsyncResult:
from result_type.async_result import from_awaitable
async def regular_async_function():
return {"data": "success"}
# Convert to AsyncResult with error handling
async_result = await from_awaitable(regular_async_function(), "Operation failed")
result = await async_result.resolve()
🆚 Comparison with Alternatives
vs PyMonad Either
# PyMonad Either (less intuitive)
from pymonad.either import Left, Right
result = Right(42) # Success
result = Left("error") # Failure
# This library (more readable)
from result_type import Success, Failure
result = Success(42) # Clear success intent
result = Failure("error") # Clear failure intent
vs Exception Handling
# Traditional exception handling
try:
result = risky_operation()
result = transform(result)
result = another_transform(result)
except Exception as e:
handle_error(e)
# With Result type
result = (
safe_call(risky_operation)
>> safe_transform
>> safe_another_transform
)
if result.is_failure():
handle_error(result.error)
🧪 Real-World Examples
Database Operations
from result_type import Result, Success, Failure
def fetch_user(user_id: str) -> Result[dict, str]:
try:
user = database.users.find_one({"_id": user_id})
if not user:
return Failure("User not found")
return Success(user)
except Exception as e:
return Failure(f"Database error: {e}")
def validate_user(user: dict) -> Result[dict, str]:
if not user.get("is_active"):
return Failure("User is inactive")
return Success(user)
def get_user_permissions(user: dict) -> Result[list, str]:
permissions = user.get("permissions", [])
if not permissions:
return Failure("User has no permissions")
return Success(permissions)
# Chain the operations
result = (
fetch_user("user123")
>> validate_user
>> get_user_permissions
)
if result.is_success():
print(f"User permissions: {result.value}")
else:
print(f"Failed to get permissions: {result.error}")
API Calls
import requests
from result_type import safe_call, Result, Success, Failure
def fetch_weather(city: str) -> Result[dict, str]:
return safe_call(
lambda: requests.get(f"http://api.weather.com/{city}").json(),
f"Failed to fetch weather for {city}"
)
def extract_temperature(weather_data: dict) -> Result[float, str]:
try:
temp = weather_data["current"]["temperature"]
return Success(float(temp))
except (KeyError, ValueError, TypeError) as e:
return Failure(f"Invalid weather data: {e}")
def celsius_to_fahrenheit(celsius: float) -> Result[float, str]:
return Success(celsius * 9/5 + 32)
# Chain API call and transformations
result = (
fetch_weather("London")
>> extract_temperature
>> celsius_to_fahrenheit
)
if result.is_success():
print(f"Temperature in Fahrenheit: {result.value}")
else:
print(f"Error: {result.error}")
File Operations
from pathlib import Path
from result_type import safe_call, Result
def read_config_file(path: str) -> Result[dict, str]:
def _read_and_parse():
content = Path(path).read_text()
return json.loads(content)
return safe_call(_read_and_parse, f"Failed to read config from {path}")
def validate_config(config: dict) -> Result[dict, str]:
required_fields = ["api_key", "database_url", "port"]
missing = [field for field in required_fields if field not in config]
if missing:
return Failure(f"Missing required fields: {missing}")
return Success(config)
def start_application(config: dict) -> Result[str, str]:
# Application startup logic here
return Success(f"Application started on port {config['port']}")
# Chain configuration loading and validation
result = (
read_config_file("config.json")
>> validate_config
>> start_application
)
if result.is_success():
print(result.value) # "Application started on port 8080"
else:
print(f"Startup failed: {result.error}")
🧪 Testing
# Install development dependencies
pip install python-result-type[dev]
# Run tests
pytest
# Run tests with coverage
pytest --cov=result_type --cov-report=html
# Run type checking
mypy result_type
# Format code
black result_type tests
📄 Type Safety
This library is fully typed and compatible with mypy:
from result_type import Result
def typed_operation(x: int) -> Result[str, str]:
if x < 0:
return Failure("Negative numbers not allowed")
return Success(str(x))
# mypy will catch type errors
result: Result[str, str] = typed_operation(42)
🤝 Contributing
Contributions are welcome! Please read our Contributing Guide for details.
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
📜 License
This project is licensed under the MIT License - see the LICENSE file for details.
🙏 Acknowledgments
- Inspired by Rust's Result type
- Similar concepts from PyMonad
- Functional programming patterns from Haskell's Either type
📈 Changelog
1.2.1
- Fixed: Mypy warning
"Result[T, E]" has no attribute "error"when accessingresult.error - Fixed: Mypy warning
"Result[T, E]" has no attribute "value"when accessingresult.value - Enhanced: Added
valueanderrorproperties to the abstractResultclass for better type checking - Improved: Better mypy compatibility and type safety throughout the codebase
- Maintained: Full backward compatibility - no breaking changes to existing code
1.2.0
- New: Rust-like aliases
OkandErrforSuccessandFailure - New: Helper functions
ok()anderr()with Rust-style naming - Enhanced: Full compatibility between Rust aliases and original naming
- Improved: Better developer experience for Rust developers
- Added: Comprehensive tests and examples for Rust-style usage
1.1.0
- New: Full async/await support with
AsyncResultwrapper - New:
async_safe_callandasync_safe_call_decoratorfor async exception handling - New: Async chaining operations with
.then_async()and.then_sync() - New:
gather_results()for concurrent async operations - New:
from_awaitable()to convert regular awaitables to AsyncResult - Improved: Enhanced type annotations for better IDE support
- Enhanced: Better error messages and stack traces
1.0.0
- Initial release
- Core Result, Success, and Failure types
- Chaining with
.then()and>>operator - Helper functions and decorators
- Comprehensive test suite
- Full type annotations
Metadata
Release files for python-result-type 1.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| python_result_type-1.2.1.tar.gz | 58.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| python_result_type-1.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 71.8 kB
Release files / python_result_type-1.2.1.tar.gz
| Download URL | python_result_type-1.2.1.tar.gz |
|---|---|
| Size | 58.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d4a519d7c1c63e0f6996c345c29652bd97b8108d3dc50c8f47451819d9afe929
|
|
BLAKE2b-256 checksum How to use checksums |
e1f1319db6818d6ac725bf8babd979d5af1937655787eb3db8ce783a0704dbca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.4
|
Release files / python_result_type-1.2.1-py3-none-any.whl
| Download URL | python_result_type-1.2.1-py3-none-any.whl |
|---|---|
| Size | 13.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4547996a3c5d7434869798e31f573896422fb00d38b7cdc802c13c171c504f37
|
|
BLAKE2b-256 checksum How to use checksums |
e905b7464291efa6681502261da6f923a757b5f50ef21a431552a7cc5db532d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.4
|
Security for python-result-type 1.2.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.