So to recap to make more clear:
Abstract
In Python, exceptions are the standard error-handling mechanism, but many modern languages (e.g., Rust, Swift) also provide a Result
type that encapsulates both success and failure in a single return value. This proposal introduces a synchronous Result
type, providing explicit error handling for cases where returning error values is preferred over raising exceptions. Additionally, a decorator will allow retrofitting existing APIs to return Result
objects without disrupting their original behavior. This feature will be introduced in Python’s functools
module, ensuring backward compatibility and seamless integration.
Updated Proposal Sections
Motivation
While exceptions are an effective and widely-used error-handling mechanism in Python, certain use cases may benefit from explicit error propagation through return values. Some modern languages offer a Result
type that can handle both success (Ok
) and failure (Error
) explicitly, making it easier to chain operations or manage errors without relying on try/except blocks at every step.
A Result
type can complement, rather than replace, Python’s exceptions. This approach is especially beneficial for developers coming from languages like Rust or Swift, where Result
patterns are common, or for scenarios where error handling needs to be explicit (e.g., functional programming or integration with external systems). This is particularly useful when:
- You want to avoid the cost of raising and catching exceptions.
- You are interacting with systems (such as Rust or C++) that use
Result
-like constructs for error handling.
- You need to compose several functions that could fail without cluttering the control flow with exception handling.
By introducing a Result
type alongside Python’s exception system, developers gain more control and flexibility in how they handle errors.
Goals
- Introduce a
Result
type that encapsulates success (Ok
) and failure (Error
) in a unified way, exposing .value
and .error
attributes for accessing results.
- Provide decorators that allow seamless integration with existing exception-based APIs, enabling these APIs to return
Result
types without breaking existing behavior.
- Ensure backward compatibility by shipping the
Result
type in functools
and keeping it fully optional for developers who prefer to continue using exceptions.
Design Overview
The Result
type will feature two main states:
Ok(value)
: Represents a successful result.
Error(error)
: Represents a failed result, encapsulating an error.
The Result
object exposes two attributes:
value
: Accesses the result if the state is Ok
. Raises an exception if the state is Error
.
error
: Accesses the error if the state is Error
. Raises an exception if the state is Ok
.
Here’s an implementation that aligns with Python’s conventions:
python
Copy code
class Result:
def __init__(self, value=None, error=None):
self._value = value
self._error = error
@classmethod
def Ok(cls, value):
return cls(value=value)
@classmethod
def Error(cls, error):
return cls(error=error)
@property
def value(self):
if self._error is not None:
raise Exception(f"Tried to access value, but error occurred: {self._error}")
return self._value
@property
def error(self):
if self._value is not None:
raise Exception("Tried to access error, but value is present.")
return self._error
def is_ok(self):
return self._error is None
def is_error(self):
return self._error is not None
Example Usage
Handling Success and Failure Explicitly:
python
Copy code
def divide(a, b):
if b == 0:
return Result.Error("Division by zero")
return Result.Ok(a / b)
result = divide(10, 2)
if result.is_ok():
print(f"Success: {result.value}")
else:
print(f"Error: {result.error}")
Auto-Unwrapping with Decorators
To ease the transition from exception-based error handling to a Result
-based approach, decorators can be used to automatically wrap return values in Result
objects. This enables existing APIs to switch to Result
handling with minimal code changes.
python
Copy code
import functools
def resultify(*exception_types):
"""Decorator to wrap specified exceptions in Result.Error."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return Result.Ok(func(*args, **kwargs))
except exception_types as e:
return Result.Error(e)
return wrapper
return decorator
Using the Decorator:
python
Copy code
@resultify(ValueError)
def divide(a, b):
if b == 0:
raise ValueError("Division by zero")
return a / b
result = divide(10, 0)
print(result.error) # Outputs: Division by zero
This approach makes the integration with Result
types straightforward without requiring major refactoring.
Backward Compatibility and Integration with Existing Code
Backward Compatibility:
- Optional use of
Result
: The Result
type will not replace exceptions. It provides an additional, explicit way to handle errors when needed, leaving Python’s current exception-based approach fully intact.
- No breaking changes: The introduction of
Result
as an opt-in feature ensures no impact on existing libraries or codebases. Developers can continue using exceptions without modification, and legacy code can be enhanced to use Result
via decorators like resultify
.
Shipping Strategy:
- Inclusion in
functools
: The Result
type and related utilities, such as resultify
, will be added to Python’s functools
module. This provides an official, well-established location for optional tools that enhance Python’s functionality without changing core language features.
Integration Examples:
- Retrofitting existing libraries: Libraries can adopt the
Result
type in a non-breaking way by wrapping existing functions using the resultify
decorator. This allows for the controlled introduction of Result
types where explicit error handling is preferred.
- Hybrid exception and
Result
use: Developers can use both patterns together. In cases where a function returns a Result
, errors can still raise exceptions when desired by simply calling the .value
attribute, and exceptions will propagate as usual.
Advantages
- Clarity: Explicit handling of success and error states leads to clearer, more readable code.
- Flexibility: Developers can opt for either the
Result
type or exceptions, depending on the use case, without forcing a specific paradigm.
- Error Propagation: Allows error handling to be more functional and structured, which is useful when chaining operations (e.g.,
.map()
or .flatmap()
could be added in future extensions).
- Compatibility: Works alongside Python’s existing exception system, maintaining backward compatibility with libraries that expect exceptions to be raised.
Not a Replacement for Exceptions
While the Result
type offers a way to handle success and failure in a more functional style, it is not intended to replace exceptions. Python’s exception model remains a key part of the language and is particularly suited for error handling when errors are exceptional and should interrupt the control flow.
The Result
type is designed for situations where:
- Errors are expected, and handling them as part of the normal control flow is important.
- You need to compose several functions that may fail without cluttering the code with
try
/except
blocks.
- Explicit error propagation is preferred, especially when working with APIs or libraries that return errors as values rather than raising exceptions.
This hybrid approach of using both exceptions and Result
allows Python to maintain its simplicity while offering more advanced error-handling tools for cases where they are needed.
Conclusion
Introducing a synchronous Result
type to Python allows developers to handle errors explicitly, improving code clarity and composability without breaking existing functionality. By shipping the Result
type and related tools in functools
, Python retains its core exception-based error-handling model while giving developers the option to use a more functional, value-based approach when needed.
This proposal enhances error handling in Python by offering flexibility, providing backward compatibility, and ensuring developers can adopt the new pattern as gradually or comprehensively as they choose.