Skip to main content

A functional programming Result type for Python with Success/Failure variants

Project description

Python Result Type

PyPI version Python Support License: MIT Tests

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: Success and Failure instead of cryptic Right/Left
  • 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
  • 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()

📚 Complete API Reference

Core Types

Result[T, E]

Abstract base class representing either success or failure.

Methods:

  • is_success() -> bool - Check if result is Success
  • is_failure() -> bool - Check if result is Failure
  • then(func: Callable[[T], Result[U, E]]) -> Result[U, E] - Chain operations
  • map(func: Callable[[T], U]) -> Result[U, E] - Transform success value
  • map_error(func: Callable[[E], F]) -> Result[T, F] - Transform error value
  • unwrap() -> T - Extract value or raise exception
  • unwrap_or(default: T) -> T - Extract value or return default
  • unwrap_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

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'>

🆚 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.

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

📜 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

📈 Changelog

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python_result_type-1.0.1.tar.gz (16.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

python_result_type-1.0.1-py3-none-any.whl (8.9 kB view details)

Uploaded Python 3

File details

Details for the file python_result_type-1.0.1.tar.gz.

File metadata

  • Download URL: python_result_type-1.0.1.tar.gz
  • Upload date:
  • Size: 16.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.4

File hashes

Hashes for python_result_type-1.0.1.tar.gz
Algorithm Hash digest
SHA256 2cebc663f181f05c9464e98ea5afa5b93a40ff0a1c2c169cde609b3559f61d93
MD5 f45ee7afd178bed4b09a3f01291b0f76
BLAKE2b-256 f82b142a20a8476b3e18ed1b6ce7b97b8801a390d523b6e24fceaa933d4ecef5

See more details on using hashes here.

File details

Details for the file python_result_type-1.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for python_result_type-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 93ee2ac15c76e68f4a73cc818c173668df4bf58748e678b93d20d0a9b1d51cfe
MD5 50c6f69c6d861d744dddc7909f92ec6f
BLAKE2b-256 2d933455bd613f8a882dcfb8a0a7508304ede4e3dbdc9b878286ee6a17411abc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page