Container is a concept that allows you to write code without traditional error handling while maintaining the execution context.
We will show you its simple API of one attribute and several simple methods.
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.
We use a concept of Railway oriented programming. It mean that our code can go on two tracks:
Successful one: where everything goes perfectly: HTTP requests work, database is always serving us data, parsing values does not failed
Failed one: where something went wrong
We can switch from track to track: we can fail something or we can rescue the situation.
We use two methods to create new containers from the previous one.
bind
and map
.
The difference is simple:
map
works with functions that return regular values
bind
works with functions that return other containers
Container.bind
is used to literally bind two different containers together.
from returns.result import Result, Success
def make_http_call(user_id: int) -> Result[int, str]:
...
result = Success(1).bind(make_http_call)
# => Will be equal to either Success[int] or Failure[str]
So, the rule is: whenever you have some impure functions, it should return a container type instead.
And we use Container.map
to use containers with pure functions.
from returns.result import Success
def double(state: int) -> int:
return state * 2
result = Success(1).map(double)
# => Will be equal to Success(2)
Note:
All containers support these methods.
We also support two special methods to work with “failed”
types like Failure
and Nothing
:
Container.fix
is the opposite of map
method
that works only when container is in failed state
Container.rescue
is the opposite of bind
method
that works only when container is in failed state
fix
can be used to fix some fixable errors
during the pipeline execution:
from returns.result import Failure
def double(state: int) -> float:
return state * 2.0
Failure(1).fix(double)
# => Will be equal to Success(2.0)
rescue
can return any container type you want.
It can also fix your flow and get on the successful track again:
from returns.result import Result, Failure, Success
def fix(state: Exception) -> Result[int, Exception]:
if isinstance(state, ZeroDivisionError):
return Success(0)
return Failure(state)
Failure(ZeroDivisionError).rescue(fix)
# => Will be equal to Success(0)
Note:
Not all containers support these methods.
IO cannot be fixed or rescued.
And we have two more functions to unwrap inner state of containers into a regular types:
Container.value_or
returns a value if it is possible, returns default_value
otherwise
Container.unwrap
returns a value if it is possible, raises UnwrapFailedError
otherwise
from returns.result import Failure, Success
Success(1).value_or(None)
# => 1
Success(0).unwrap()
# => 0
Failure(1).value_or(default_value=100)
# => 100
Failure(1).unwrap()
# => Traceback (most recent call last): UnwrapFailedError
The most user-friendly way to use unwrap
method is with pipeline.
For failing containers you can
use Container.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.
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.
We try to make our containers optionally type safe.
What does it mean?
It is still good old python
, do whatever you want without mypy
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.
However, this is still good old python
type system,
and it has its drawbacks.
Container
(inner_value)[source]¶Bases: returns.primitives.container._BaseContainer
Represents a “context” in which calculations can be executed.
You won’t create ‘Container’ instances directly. Instead, sub-classes implement specific contexts. containers allow you to bind together a series of calculations while maintaining the context of that specific container.
This is an abstract class with the API declaration.
_inner_value
¶Wrapped internal immutable state.
GenericContainerOneSlot
(inner_value)[source]¶Bases: typing.Generic
, returns.primitives.container.Container
Base class for containers with one typed slot.
Use this type for generic inheritance only.
Use Container
as a general type for polymorphism.
GenericContainerTwoSlots
(inner_value)[source]¶Bases: typing.Generic
, returns.primitives.container.Container
Base class for containers with two typed slot.
Use this type for generic inheritance only.
Use Container
as a general type for polymorphism.