Skip to content

Outcome

The fault-tolerant Product Type for accumulating diagnostics and partial states.

result.outcome

Outcome: Product Type Error Handling for Modern Python.

This module provides the Outcome type, a fault-tolerant product type that holds both a success value and an optional error state simultaneously.

Outcome

Bases: NamedTuple

A fault-tolerant Product Type holding a value and an error state.

'error' can be a single Exception, a Collection of Exceptions, or None. Unlike Result, an Outcome always contains a value, even if errors occurred.

Attributes:

Name Type Description
value T

The success or partial success value.

error E | Any | None

The error state or diagnostic baggage.

unsafe property

unsafe: _OutcomeUnsafe[T, E]

Namespace for operations that bypass standard type safety.

Examples:

>>> Outcome(10, None).unsafe.cast_types[int, Exception]()

__add__

__add__(other: Any) -> Outcome[Any, Any]

Support addition (+) operator.

Combines values using + and accumulates any errors.

Source code in src/result/outcome.py
def __add__(self, other: Any) -> Outcome[Any, Any]:
    """Support addition (+) operator.

    Combines values using + and accumulates any errors.
    """
    if isinstance(other, Outcome):
        errs: list[Any] = []
        if self.error is not None:
            if isinstance(self.error, list):
                errs.extend(self.error)
            else:
                errs.append(self.error)
        if other.error is not None:
            if isinstance(other.error, list):
                errs.extend(other.error)
            else:
                errs.append(other.error)
        return Outcome(self.value + other.value, errs or None)  # ty:ignore[unused-type-ignore-comment, unused-ignore-comment]
    return NotImplemented

__bool__

__bool__() -> bool

Truthiness check. Returns True if there are no errors.

Source code in src/result/outcome.py
def __bool__(self) -> bool:
    """Truthiness check. Returns True if there are no errors."""
    return not self.has_error()

and_then

and_then(
    func: Callable[[T], Outcome[U, E2]],
) -> Outcome[U, E | E2]

The monadic bind for Outcomes.

Applies the function to the value and concatenates errors from both Outcomes.

Parameters:

Name Type Description Default
func Callable[[T], Outcome[U, E2]]

A function returning a new Outcome.

required

Returns:

Type Description
Outcome[U, E | E2]

A new Outcome with the transformed value and combined errors.

Examples:

>>> out = Outcome(10, "e1")
>>> out.and_then(lambda x: Outcome(x + 1, "e2"))
Outcome(value=11, error=['e1', 'e2'])
Source code in src/result/outcome.py
def and_then[U, E2](self, func: Callable[[T], Outcome[U, E2]]) -> Outcome[U, E | E2]:
    """The monadic bind for Outcomes.

    Applies the function to the value and concatenates errors from both Outcomes.

    Args:
        func: A function returning a new Outcome.

    Returns:
        A new Outcome with the transformed value and combined errors.

    Examples:
        >>> out = Outcome(10, "e1")
        >>> out.and_then(lambda x: Outcome(x + 1, "e2"))
        Outcome(value=11, error=['e1', 'e2'])

    """
    new_outcome = func(self.value)

    errs: list[Any] = []
    if self.error is not None:
        if isinstance(self.error, list):
            errs.extend(self.error)
        else:
            errs.append(self.error)

    if new_outcome.error is not None:
        if isinstance(new_outcome.error, list):
            errs.extend(new_outcome.error)
        else:
            errs.append(new_outcome.error)

    final_err = errs or None
    return cast("Any", Outcome(new_outcome.value, final_err))  # type: ignore[no-any-return]  # ty:ignore[unused-type-ignore-comment, unused-type-ignore-comment, unused-ignore-comment]

cast_types

cast_types() -> Outcome[U, F]

Zero-runtime-cost type hint override for strict variance edge cases.

This allows manually guiding the type checker when it fails to infer complex union types correctly.

Returns:

Type Description
Outcome[U, F]

The same instance, but with new type parameters for the checker.

Source code in src/result/outcome.py
def cast_types[U, F](self) -> Outcome[U, F]:
    """Zero-runtime-cost type hint override for strict variance edge cases.

    This allows manually guiding the type checker when it fails to infer
    complex union types correctly.

    Returns:
        The same instance, but with new type parameters for the checker.

    """
    return cast("Outcome[U, F]", self)

has_error

has_error() -> bool

Check if an error exists, intelligently handling empty collections.

Returns:

Type Description
bool

True if error is not None and (if it's a collection) is not empty.

Examples:

>>> Outcome(10, None).has_error()
False
>>> Outcome(10, ValueError()).has_error()
True
>>> Outcome(10, []).has_error()
False
Source code in src/result/outcome.py
def has_error(self) -> bool:
    """Check if an error exists, intelligently handling empty collections.

    Returns:
        True if error is not None and (if it's a collection) is not empty.

    Examples:
        >>> Outcome(10, None).has_error()
        False
        >>> Outcome(10, ValueError()).has_error()
        True
        >>> Outcome(10, []).has_error()
        False

    """
    if self.error is None:
        return False

    # Intelligent collection check (excluding str/bytes)
    if isinstance(self.error, Collection) and not isinstance(self.error, str | bytes):
        return len(self.error) > 0

    return True

map

map(func: Callable[[T], U_inner]) -> Outcome[U_inner, E]

Transform the success value while preserving the error state.

Parameters:

Name Type Description Default
func Callable[[T], U_inner]

A function to transform the value.

required

Returns:

Type Description
Outcome[U_inner, E]

A new Outcome with the transformed value and the same error.

Examples:

>>> Outcome(10, "warning").map(str)
Outcome(value='10', error='warning')
Source code in src/result/outcome.py
def map[U_inner](self, func: Callable[[T], U_inner]) -> Outcome[U_inner, E]:
    """Transform the success value while preserving the error state.

    Args:
        func: A function to transform the value.

    Returns:
        A new Outcome with the transformed value and the same error.

    Examples:
        >>> Outcome(10, "warning").map(str)
        Outcome(value='10', error='warning')

    """
    return Outcome(func(self.value), self.error)

map_err

map_err(func: Callable[[E], F]) -> Outcome[T, F]

Transform the error payload.

If the error is a collection, it intelligently maps the function over every item in the collection.

Parameters:

Name Type Description Default
func Callable[[E], F]

A function to transform the error(s).

required

Returns:

Type Description
Outcome[T, F]

A new Outcome with the transformed error state.

Examples:

>>> Outcome(10, ValueError("fail")).map_err(lambda e: str(e))
Outcome(value=10, error='fail')
>>> # Mapping over accumulated errors
>>> out = Outcome(10, [ValueError("a"), KeyError("b")])
>>> out.map_err(type)
Outcome(value=10, error=[<class 'ValueError'>, <class 'KeyError'>])
Source code in src/result/outcome.py
def map_err[F](self, func: Callable[[E], F]) -> Outcome[T, F]:
    """Transform the error payload.

    If the error is a collection, it intelligently maps the function
    over every item in the collection.

    Args:
        func: A function to transform the error(s).

    Returns:
        A new Outcome with the transformed error state.

    Examples:
        >>> Outcome(10, ValueError("fail")).map_err(lambda e: str(e))
        Outcome(value=10, error='fail')

        >>> # Mapping over accumulated errors
        >>> out = Outcome(10, [ValueError("a"), KeyError("b")])
        >>> out.map_err(type)
        Outcome(value=10, error=[<class 'ValueError'>, <class 'KeyError'>])

    """
    if self.error is None:
        return Outcome(self.value, None)

    if isinstance(self.error, list):
        return cast("Any", Outcome(self.value, [func(e) for e in self.error]))  # type: ignore[no-any-return] # ty:ignore[unused-type-ignore-comment, unused-ignore-comment]]

    return Outcome(self.value, func(self.error))  # pyright: ignore[reportArgumentType]

map_exc

map_exc(
    mapping: Mapping[type[Exception], Any],
) -> Outcome[T, Any]

Transform specific exception types in the error payload.

This is ideal for converting raw Python exceptions into domain-specific Enums or error codes within a fault-tolerant Outcome.

Parameters:

Name Type Description Default
mapping Mapping[type[Exception], Any]

A dictionary mapping exception types to new values.

required

Returns:

Type Description
Outcome[T, Any]

A new Outcome with the mapped error if a match was found, otherwise self.

Examples:

>>> # Single error mapping
>>> Outcome(10, ValueError("fail")).map_exc({ValueError: ErrorCode.INVALID})
Outcome(value=10, error=<ErrorCode.INVALID: 'invalid'>)
>>> # Collection mapping (accumulated diagnostics)
>>> out = Outcome(10, [ValueError("a"), KeyError("b")])
>>> out.map_exc({ValueError: "err_val", KeyError: "err_key"})
Outcome(value=10, error=['err_val', 'err_key'])
Source code in src/result/outcome.py
def map_exc(self, mapping: Mapping[type[Exception], Any]) -> Outcome[T, Any]:
    """Transform specific exception types in the error payload.

    This is ideal for converting raw Python exceptions into domain-specific
    Enums or error codes within a fault-tolerant Outcome.

    Args:
        mapping: A dictionary mapping exception types to new values.

    Returns:
        A new Outcome with the mapped error if a match was found, otherwise self.

    Examples:
        >>> # Single error mapping
        >>> Outcome(10, ValueError("fail")).map_exc({ValueError: ErrorCode.INVALID})
        Outcome(value=10, error=<ErrorCode.INVALID: 'invalid'>)

        >>> # Collection mapping (accumulated diagnostics)
        >>> out = Outcome(10, [ValueError("a"), KeyError("b")])
        >>> out.map_exc({ValueError: "err_val", KeyError: "err_key"})
        Outcome(value=10, error=['err_val', 'err_key'])

    """
    if not self.has_error():
        return self

    # Handle collection of errors
    if isinstance(self.error, Collection) and not isinstance(self.error, str | bytes):  # pyright: ignore[reportUnknownMemberType]
        new_errors = [mapping.get(type(e), e) if isinstance(e, Exception) else e for e in self.error]  # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
        return Outcome(self.value, new_errors)

    if isinstance(self.error, Exception):
        err_type = type(self.error)
        if err_type in mapping:
            return Outcome(self.value, mapping[err_type])

    return self

merge

merge(
    other: Outcome[U, E2],
) -> Outcome[tuple[T, U], E | E2]

Combine two independent outcomes.

Zips the values into a tuple and concatenates all accumulated errors.

Parameters:

Name Type Description Default
other Outcome[U, E2]

Another Outcome to merge with.

required

Returns:

Type Description
Outcome[tuple[T, U], E | E2]

A merged Outcome with tuple value and combined errors.

Examples:

>>> out1 = Outcome(1, "e1")
>>> out2 = Outcome(2, "e2")
>>> out1.merge(out2)
Outcome(value=(1, 2), error=['e1', 'e2'])
Source code in src/result/outcome.py
def merge[U, E2](self, other: Outcome[U, E2]) -> Outcome[tuple[T, U], E | E2]:
    """Combine two independent outcomes.

    Zips the values into a tuple and concatenates all accumulated errors.

    Args:
        other: Another Outcome to merge with.

    Returns:
        A merged Outcome with tuple value and combined errors.

    Examples:
        >>> out1 = Outcome(1, "e1")
        >>> out2 = Outcome(2, "e2")
        >>> out1.merge(out2)
        Outcome(value=(1, 2), error=['e1', 'e2'])

    """
    errs: list[Any] = []

    # Collect self errors
    if self.error is not None:
        if isinstance(self.error, list):
            errs.extend(self.error)
        else:
            errs.append(self.error)

    # Collect other errors
    if other.error is not None:
        if isinstance(other.error, list):
            errs.extend(other.error)
        else:
            errs.append(other.error)

    final_err = errs or None
    return cast("Any", Outcome((self.value, other.value), final_err))  # type: ignore[no-any-return]  # ty:ignore[unused-type-ignore-comment, unused-type-ignore-comment, unused-ignore-comment]

or_else

or_else(
    func: Callable[[E | Any | None], Outcome[T, F]],
) -> Outcome[T, F]

Recovery hatch for Outcomes with errors.

If errors exist, they are passed to the function to attempt recovery.

Parameters:

Name Type Description Default
func Callable[[E | Any | None], Outcome[T, F]]

A function taking the current error state and returning a new Outcome.

required

Returns:

Type Description
Outcome[T, F]

The current instance if clean, or the result of func if errors exist.

Examples:

>>> out = Outcome(0, "fail")
>>> out.or_else(lambda _: Outcome(1, None))
Outcome(value=1, error=None)
Source code in src/result/outcome.py
def or_else[F](self, func: Callable[[E | Any | None], Outcome[T, F]]) -> Outcome[T, F]:
    """Recovery hatch for Outcomes with errors.

    If errors exist, they are passed to the function to attempt recovery.

    Args:
        func: A function taking the current error state and returning a new Outcome.

    Returns:
        The current instance if clean, or the result of func if errors exist.

    Examples:
        >>> out = Outcome(0, "fail")
        >>> out.or_else(lambda _: Outcome(1, None))
        Outcome(value=1, error=None)

    """
    if self.has_error():
        return func(self.error)
    return cast("Outcome[T, F]", self)

push_err

push_err(new_error: E2) -> Outcome[T, E | E2]

Append a new diagnostic error without altering the value.

If the current error is a list, the new error is appended. If it's a single value, both are wrapped in a new list.

Parameters:

Name Type Description Default
new_error E2

The new error to accumulate.

required

Returns:

Type Description
Outcome[T, E | E2]

A new Outcome with the expanded error state.

Examples:

>>> Outcome(10, None).push_err("err1")
Outcome(value=10, error='err1')
>>> Outcome(10, "err1").push_err("err2")
Outcome(value=10, error=['err1', 'err2'])
Source code in src/result/outcome.py
def push_err[E2](self, new_error: E2) -> Outcome[T, E | E2]:
    """Append a new diagnostic error without altering the value.

    If the current error is a list, the new error is appended.
    If it's a single value, both are wrapped in a new list.

    Args:
        new_error: The new error to accumulate.

    Returns:
        A new Outcome with the expanded error state.

    Examples:
        >>> Outcome(10, None).push_err("err1")
        Outcome(value=10, error='err1')
        >>> Outcome(10, "err1").push_err("err2")
        Outcome(value=10, error=['err1', 'err2'])

    """
    if self.error is None:
        return cast("Any", Outcome(self.value, new_error))  # type: ignore[no-any-return]  # ty:ignore[unused-type-ignore-comment, unused-type-ignore-comment, unused-ignore-comment]

    if isinstance(self.error, list):
        return cast("Any", Outcome(self.value, [*self.error, new_error]))  # type: ignore[no-any-return]  # ty:ignore[unused-type-ignore-comment, unused-type-ignore-comment, unused-ignore-comment]

    return cast("Any", Outcome(self.value, [self.error, new_error]))  # type: ignore[no-any-return]  # ty:ignore[unused-type-ignore-comment, unused-type-ignore-comment, unused-ignore-comment]

tap_err

tap_err(
    func: Callable[[E | Any | None], Any],
) -> Outcome[T, E]

Execute a side-effect only if an error exists.

Parameters:

Name Type Description Default
func Callable[[E | Any | None], Any]

A function called with the error state if it exists.

required

Returns:

Type Description
Outcome[T, E]

The current Outcome unchanged.

Examples:

>>> Outcome(10, "fail").tap_err(print)
fail
Outcome(value=10, error='fail')
Source code in src/result/outcome.py
def tap_err(self, func: Callable[[E | Any | None], Any]) -> Outcome[T, E]:
    """Execute a side-effect only if an error exists.

    Args:
        func: A function called with the error state if it exists.

    Returns:
        The current Outcome unchanged.

    Examples:
        >>> Outcome(10, "fail").tap_err(print)
        fail
        Outcome(value=10, error='fail')

    """
    if self.has_error():
        func(self.error)
    return self

to_ok

to_ok() -> Ok[T]

Discard any error state and return the value wrapped in Ok.

Returns:

Type Description
Ok[T]

An Ok variant containing the value.

Source code in src/result/outcome.py
def to_ok(self) -> Ok[T]:
    """Discard any error state and return the value wrapped in Ok.

    Returns:
        An Ok variant containing the value.

    """
    return Ok(self.value)

to_result

to_result() -> Result[T, E]

Convert the Outcome into a strict Sum Type (Result).

Returns:

Type Description
Result[T, E]

Err(error) if errors exist, otherwise Ok(value).

Examples:

>>> Outcome(10, None).to_result()
Ok(10)
>>> Outcome(10, ValueError("fail")).to_result()
Err(ValueError('fail'))
Source code in src/result/outcome.py
def to_result(self) -> Result[T, E]:
    """Convert the Outcome into a strict Sum Type (Result).

    Returns:
        Err(error) if errors exist, otherwise Ok(value).

    Examples:
        >>> Outcome(10, None).to_result()
        Ok(10)
        >>> Outcome(10, ValueError("fail")).to_result()
        Err(ValueError('fail'))

    """
    if self.has_error():
        # We know error is not None here because has_error() checked it
        return Err(self.error)  # type: ignore[arg-type]

    return Ok(self.value)

as_outcome

as_outcome(
    exception: E_in,
    default: T,
    mapping: type[Exception]
    | tuple[type[Exception], ...]
    | Mapping[type[Exception], E_out]
    | None = None,
    *,
    map_to: E_out | None = None,
) -> Outcome[T, E_in | E_out]

Lift a caught exception into an Outcome variant with a default value.

This pinpoint utility allows manual conversion inside standard try/except blocks while preserving the fault-tolerant Outcome pattern.

Parameters:

Name Type Description Default
exception E_in

The exception instance to wrap.

required
default T

The success value to carry in the Outcome.

required
mapping type[Exception] | tuple[type[Exception], ...] | Mapping[type[Exception], E_out] | None

Optional exception type, tuple, or dict for transformation.

None
map_to E_out | None

Optional value to use as the error if mapping is a type/tuple.

None

Returns:

Type Description
Outcome[T, E_in | E_out]

An Outcome containing the default value and the (mapped) error.

Examples:

>>> try:
...     raise ValueError("fail")
... except Exception as e:
...     out = as_outcome(e, default=0, mapping={ValueError: "mapped"})
>>> out
Outcome(value=0, error='mapped')
Source code in src/result/outcome.py
def as_outcome[T, E_in: Exception, E_out](
    exception: E_in,
    default: T,
    mapping: type[Exception] | tuple[type[Exception], ...] | Mapping[type[Exception], E_out] | None = None,
    *,
    map_to: E_out | None = None,
) -> Outcome[T, E_in | E_out]:
    """Lift a caught exception into an Outcome variant with a default value.

    This pinpoint utility allows manual conversion inside standard try/except
    blocks while preserving the fault-tolerant Outcome pattern.

    Args:
        exception: The exception instance to wrap.
        default: The success value to carry in the Outcome.
        mapping: Optional exception type, tuple, or dict for transformation.
        map_to: Optional value to use as the error if mapping is a type/tuple.

    Returns:
        An Outcome containing the default value and the (mapped) error.

    Examples:
        >>> try:
        ...     raise ValueError("fail")
        ... except Exception as e:
        ...     out = as_outcome(e, default=0, mapping={ValueError: "mapped"})
        >>> out
        Outcome(value=0, error='mapped')

    """
    if mapping is None:
        return cast("Outcome[T, E_in | E_out]", Outcome(default, exception))

    exc_map = _resolve_mapping(mapping, map_to)
    has_mapping = map_to is not None or isinstance(mapping, Mapping)

    if type(exception) in exc_map:
        mapped = exc_map[type(exception)] if has_mapping else exception
        return cast("Outcome[T, E_in | E_out]", Outcome(default, mapped))

    return cast("Outcome[T, E_in | E_out]", Outcome(default, exception))

catch_outcome

catch_outcome(
    exceptions: type[E], default: T, *, map_to: Any = None
) -> Callable[
    [Callable[P, T_ret]],
    Callable[P, Outcome[T | T_ret, Any]],
]
catch_outcome(
    exceptions: Mapping[type[Exception], Any], default: T
) -> Callable[
    [Callable[P, T_ret]],
    Callable[P, Outcome[T | T_ret, Any]],
]
catch_outcome(
    exceptions: tuple[type[Exception], ...],
    default: T,
    *,
    map_to: Any = None,
) -> Callable[
    [Callable[P, T_ret]],
    Callable[P, Outcome[T | T_ret, Any]],
]
catch_outcome(
    exceptions: Any, default: Any, *, map_to: Any = None
) -> Any

Catch exceptions and return an Outcome with a fallback value.

Requires a 'default' value to populate the Outcome payload on crash.

Parameters:

Name Type Description Default
exceptions Any

An exception type, tuple of types, or mapping of types to values.

required
default Any

The fallback value to use if an exception is caught.

required
map_to Any

Optional value to use as the error if an exception matches.

None

Returns:

Type Description
Any

A decorator that wraps a function to return an Outcome.

Examples:

>>> # 1. Simple catch with default
>>> @catch_outcome(ValueError, default=0)
... def parse(s: str) -> int:
...     return int(s)
>>> parse("abc")
Outcome(value=0, error=ValueError(...))
>>> # 2. Map single error using map_to
>>> @catch_outcome(ValueError, default=-1, map_to=ErrorCode.INVALID)
... def risky_parse(s: str) -> int:
...     return int(s)
>>> risky_parse("abc")
Outcome(value=-1, error=<ErrorCode.INVALID: 'invalid'>)
>>> # 3. Map multiple errors using a dictionary
>>> error_map = {ValueError: ErrorCode.INVALID, KeyError: ErrorCode.MISSING}
>>> @catch_outcome(error_map, default=None)
... def complex_op(x):
...     if x == 0:
...         raise ValueError
...     if x == 1:
...         raise KeyError
...     return "ok"
>>> complex_op(0)
Outcome(value=None, error=<ErrorCode.INVALID: 'invalid'>)
Source code in src/result/outcome.py
def catch_outcome(
    exceptions: Any,
    default: Any,
    *,
    map_to: Any = None,
) -> Any:
    """Catch exceptions and return an Outcome with a fallback value.

    Requires a 'default' value to populate the Outcome payload on crash.

    Args:
        exceptions: An exception type, tuple of types, or mapping of types to values.
        default: The fallback value to use if an exception is caught.
        map_to: Optional value to use as the error if an exception matches.

    Returns:
        A decorator that wraps a function to return an Outcome.

    Examples:
        >>> # 1. Simple catch with default
        >>> @catch_outcome(ValueError, default=0)
        ... def parse(s: str) -> int:
        ...     return int(s)
        >>> parse("abc")
        Outcome(value=0, error=ValueError(...))

        >>> # 2. Map single error using map_to
        >>> @catch_outcome(ValueError, default=-1, map_to=ErrorCode.INVALID)
        ... def risky_parse(s: str) -> int:
        ...     return int(s)
        >>> risky_parse("abc")
        Outcome(value=-1, error=<ErrorCode.INVALID: 'invalid'>)

        >>> # 3. Map multiple errors using a dictionary
        >>> error_map = {ValueError: ErrorCode.INVALID, KeyError: ErrorCode.MISSING}
        >>> @catch_outcome(error_map, default=None)
        ... def complex_op(x):
        ...     if x == 0:
        ...         raise ValueError
        ...     if x == 1:
        ...         raise KeyError
        ...     return "ok"
        >>> complex_op(0)
        Outcome(value=None, error=<ErrorCode.INVALID: 'invalid'>)

    """
    exc_map = _resolve_mapping(exceptions, map_to)
    catch_tuple = tuple(exc_map.keys())
    # Track if we have an actual mapping value (other than the exception class itself)
    has_mapping = map_to is not None or isinstance(exceptions, Mapping)

    def decorator(func: Any) -> Any:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Outcome[Any, Any]:
            __tracebackhide__ = True
            try:
                return Outcome(func(*args, **kwargs), None)
            except catch_tuple as e:
                # If we have an explicit mapping, use it. Otherwise, use the instance 'e'.
                mapped = exc_map[type(e)] if has_mapping else e
                return Outcome(default, mapped)
            except Exception as e:  # noqa: BLE001
                # Hide the decorator frame from the traceback in modern tools
                # and suppress implementation-detail exception context.
                tb = e.__traceback__
                raise e.with_traceback(tb.tb_next if tb else None) from None

        return wrapper

    return decorator