Container: the concept

Container is a concept that allows you to write code around the existing wrapped values while maintaining the execution context.

List of supported containers:

  • Maybe to handle None cases

  • IO to mark explicit IO actions

  • Result to handle possible exceptions

We will show you container’s simple API of one attribute and several simple methods.

Basics

The main idea behind a container is that it wraps some internal state. That’s what ._inner_value is used for.

And we have several functions to create new containers based on the previous state. And we can see how this state is evolving during the execution.

graph LR F1["Container(Initial)"] --> F2["Container(UserId(1))"] F2 --> F3["Container(UserAccount(156))"] F3 --> F4["Container(FailedLoginAttempt(1))"] F4 --> F5["Container(SentNotificationId(992))"]

State evolution.

Working with containers

We use two methods to create a new container from the previous one. bind and map.

The difference is simple:

  • map works with functions that return regular value

  • bind works with functions that return new container of the same type

bind() is used to literally bind two different containers together.

from returns.result import Result, Success

def may_fail(user_id: int) -> Result[float, str]:
    ...

value: Result[int, str] = Success(1)
# Can be assumed as either Success[float] or Failure[str]:
result: Result[float, str] = value.bind(may_fail)

And we have map() to use containers with regular functions.

from typing import Any
from returns.result import Success, Result

def double(state: int) -> int:
    return state * 2

result: Result[int, Any] = Success(1).map(double)
# => Success(2)
result: Result[int, Any] = result.map(lambda state: state + 1)
# => Success(3)

The same work with built-in functions as well:

from returns.io import IO

IO('bytes').map(list)
# => <IO: ['b', 'y', 't', 'e', 's']>

Note:

All containers support these methods.

Railway oriented programming

When talking about error handling we use a concept of Railway oriented programming. It mean that our code can go on two tracks:

  1. Successful one: where everything goes perfectly: HTTP requests work, database is always serving us data, parsing values does not failed

  2. Failed one: where something went wrong

We can switch from track to track: we can fail something or we can fix the situation.

graph LR S1 -- Map --> S3 S3 --> S5 S5 --> S7 F2 -- Alt --> F4 F4 --> F6 F6 --> F8 S1 -- Fail --> F2 F2 -- Fix --> S3 S3 -- Fail --> F4 S5 -- Fail --> F6 F6 -- Fix --> S7 style S1 fill:green style S3 fill:green style S5 fill:green style S7 fill:green style F2 fill:red style F4 fill:red style F6 fill:red style F8 fill:red

Railway oriented programming.

Returning execution to the right track

We also support two special methods to work with “failed” types like Failure:

  • rescue() is the opposite of bind method that works only when container is in failed state

  • fix() transforms error to value (failure became success) that works only when container is in failed state, is the opposite of map method

  • alt() transforms error to another error that works only when container is in failed state, is the opposite of map method

fix can be used to fix some fixable errors during the pipeline execution:

from returns.result import Failure, Result

def double(state: int) -> float:
    return state * 2.0

result: Result[Any, float] = Failure(1).alt(double)
# => Failure(2.0)

result: Result[float, int] = Failure(1).fix(double)
# => Success(2.0)

rescue should return one of Success or Failure types. It can also rescue your flow and get on the successful track again:

from returns.result import Result, Failure, Success

def tolerate_exception(state: Exception) -> Result[int, Exception]:
    if isinstance(state, ZeroDivisionError):
        return Success(0)
    return Failure(state)

value: Result[int, Exception] = Failure(ZeroDivisionError())
result: Result[int, Exception] = value.rescue(tolerate_exception)
# => Success(0)

value2: Result[int, Exception] = Failure(ValueError())
result2: Result[int, Exception] = value2.rescue(tolerate_exception)
# => Failure(ValueError())

Note:

Not all containers support these methods.
IO cannot be fixed or rescued.

Unwrapping values

And we have two more functions to unwrap inner state of containers into a regular types:

  • .value_or returns a value if it is possible, returns default_value otherwise

  • .unwrap returns a value if it is possible, raises UnwrapFailedError otherwise

from returns.result import Failure, Success
from returns.maybe import Some, Nothing

Success(1).value_or(None)
# => 1

Some(0).unwrap()
# => 0

Failure(1).value_or(default_value=100)
# => 100

Failure(1).unwrap()
# => Traceback (most recent call last): UnwrapFailedError

Nothing.unwrap()
# => Traceback (most recent call last): UnwrapFailedError

The most user-friendly way to use .unwrap() method is with pipeline. We even discourage using .unwrap() without a @pipeline.

For failing containers you can use .failure to unwrap the failed state:

Failure(1).failure()
# => 1

Success(1).failure()
# => Traceback (most recent call last): UnwrapFailedError

Be careful, since this method will raise an exception when you try to .failure() a successful container.

Note:

Not all containers support these methods.
IO cannot be unwrapped.

Immutability

We like to think of returns as immutable structures. You cannot mutate the inner state of the created container, because we redefine __setattr__ and __delattr__ magic methods.

You cannot also set new attributes to container instances, since we are using __slots__ for better performance and strictness.

Well, nothing is really immutable in python, but you were warned.

Type safety

We try to make our containers optionally type safe.

What does it mean?

  1. It is still good old python, do whatever you want without mypy

  2. If you are using mypy you will be notified about type violations

We also ship PEP561 compatible .pyi files together with the source code. In this case these types will be available to users when they install our application.

We also ship custom mypy plugins to overcome some existing problems, please make sure to use them, since they increase your developer experience and type-safety:

[mypy]
plugins =
  returns.contrib.mypy.decorator_plugin

You can have a look at the suggested mypy configuration in our own repository.

Composition

You can and should compose different containers together. Here’s the full table of compositions that make sense:

  • IO[Result[A, B]]

  • IO[Maybe[A]]

  • IO[IO[A]] 🤔, use join

  • Maybe[Maybe[A]] 🤔, use join

  • Result[Result[A, B], C] 🤔, use join

  • Result[Maybe[A], B] 🤔,

    use maybe_to_result

  • Maybe[Result[A, B]] 🤔,

    use result_to_maybe

  • Result[IO[A], B] 🚫

  • Result[A, IO[A]] 🚫

  • Result[A, Maybe[B]] 🚫

  • Result[A, Result[B, C]] 🚫

  • Maybe[IO[A]] 🚫

You can use Converters to convert Maybe and Result containers. So, you don’t have to compose them.

You can also use join to merge nested containers.

Converters

We have several helper functions to convert containers from Maybe to Result and back again:

  • maybe_to_result that converts Maybe to Result

  • result_to_maybe that converts Result to Maybe

That’s how they work:

from returns.converters import maybe_to_result, result_to_maybe
from returns.maybe import Maybe
from returns.result import Result

result: Result[int, Exception]
maybe: Maybe[int] = result_to_maybe(result)
new_result: Result[int, None] = maybe_to_result(maybe)

Take a note, that type changes. Also, take a note that Success(None) will be converted to Nothing.

join

You can also use join to merge nested containers together:

from returns.converters import join
from returns.maybe import Maybe
from returns.result import Success
from returns.io import IO

assert join(IO(IO(1))) == IO(1)
assert join(Maybe(Maybe(1))) == Maybe(1)
assert join(Success(Success(1))) == Success(1)

coalesce

You can use returns.converters.coalesce_result() and returns.converters.coalesce_maybe() converters to covert containers to a regular value.

These functions accept two functions: one for successful case, one for failing case.

from returns.converters import coalesce_result
from returns.result import Success, Failure

def handle_success(state: int) -> float:
    return state / 2

def handle_failure(state: str) -> float:
    return 0.0

coalesce_result(handle_success, handle_failure)(Success(1))
# => returns `0.5`
coalesce_result(handle_success, handle_failure)(Failure(1))
# => returns `0.0`

API Reference

graph TD; UnwrapableFailure Bindable Fixable Unwrapable Rescueable Mappable BaseContainer Protocol --> Bindable Protocol --> Mappable Protocol --> Fixable Protocol --> Rescueable Protocol --> Unwrapable Protocol --> UnwrapableFailure
class BaseContainer(inner_value)[source]

Bases: object

Utility class to provide all needed magic methods to the context.

class Bindable(*args, **kwargs)[source]

Bases: typing_extensions.Protocol

Represents a “context” in which calculations can be executed.

Bindable allows you to bind together a series of calculations while maintaining the context of that specific container.

bind(function)[source]

Applies ‘function’ to the result of a previous calculation.

And returns a new container. Works for containers that represent success. Is the opposite of Rescueable.rescue().

Return type

Bindable[~_NewValueType]

class Mappable(*args, **kwargs)[source]

Bases: typing_extensions.Protocol

Allows to chain wrapped values with regular functions.

Behaves like functor.

map(function)[source]

Applies ‘function’ to the contents of the functor.

And returns a new functor value. Is the opposite of Fixable.fix().

Return type

Mappable[~_NewValueType]

class Fixable(*args, **kwargs)[source]

Bases: typing_extensions.Protocol

Represents containers that can be fixed and rescued.

fix(function)[source]

Applies ‘function’ to the error and transforms failure to success.

And returns a new functor value. Works for containers that represent failure. Is the opposite of Mappable.map().

Return type

Fixable[~_NewValueType, +_ErrorType]

class Rescueable(*args, **kwargs)[source]

Bases: typing_extensions.Protocol

Represents a “context” in which calculations can be executed.

Rescueable allows you to bind together a series of calculations while maintaining the context of that specific container.

rescue(function)[source]

Applies ‘function’ to the result of a previous calculation.

And returns a new container. Works for containers that represent failure. Is the opposite of bind().

Return type

Rescueable[~_NewValueType, ~_NewErrorType]

class Unwrapable(*args, **kwargs)[source]

Bases: typing_extensions.Protocol

Represents containers that can unwrap and return its wrapped value.

value_or(default_value)[source]

Forces to unwrap value from container or return a default.

Return type

Union[+_ValueType, ~_NewValueType]

unwrap()[source]

Custom magic method to unwrap inner value from container.

Should be redefined for ones that actually have values. And for ones that raise an exception for no values.

This method is the opposite of failure().

Return type

+_ValueType

failure()[source]

Custom magic method to unwrap inner value from the failed container.

This method is the opposite of unwrap().

Return type

+_ErrorType

class UnwrapableFailure(*args, **kwargs)[source]

Bases: typing_extensions.Protocol

Allows to unwrap failures.

alt(function)[source]

Uses ‘function’ to transform one error to another.

And returns a new functor value. Works for containers that represent failure. Is the opposite of map().

Return type

Fixable[+_ValueType, ~_NewErrorType]

result_to_maybe(result_container)[source]

Converts Result container to Maybe container.

>>> from returns.maybe import Some, Nothing
>>> from returns.result import Failure, Success
>>> result_to_maybe(Success(1)) == Some(1)
True
>>> result_to_maybe(Failure(1)) == Nothing
True
>>> result_to_maybe(Success(None)) == Nothing
True
Return type

Maybe[~_ValueType]

maybe_to_result(maybe_container)[source]

Converts Maybe container to Result container.

>>> from returns.maybe import Some, Nothing
>>> from returns.result import Failure, Success
>>> maybe_to_result(Nothing) == Failure(None)
True
>>> maybe_to_result(Some(1)) == Success(1)
True
>>> maybe_to_result(Some(None)) == Failure(None)
True
Return type

Result[~_ValueType]

join(container)[source]

Joins two nested containers together.

>>> from returns.maybe import Some
>>> from returns.result import Success
>>> from returns.io import IO
>>> join(IO(IO(1))) == IO(1)
True
>>> join(Some(Some(1))) == Some(1)
True
>>> join(Success(Success(1))) == Success(1)
True
Features Maybe