returns

repository·master·Indexed 26 days ago

https://github.com/dry-python/returns

A functional programming library for Python (v0.28.0) that provides containers like Maybe, Result, IO, Future, and RequiresContext to write safer, more declarative, and typed business logic. It includes tools for handling None values, managing exceptions via the @safe decorator, separating pure logic from side-effects, and composing asynchronous operations using FutureResultE. The library supports mypy through a dedicated plugin for enhanced type-safety.

Tokens
41K
Snippets
117
Records
179
Agent score
85%

What's inside returns

  1. Overview of returns core containers and abstractions

    master

    The returns library provides several abstractions to solve common Python development problems:

    • Maybe: Handles None values in a type-safe manner.
    • Result: Handles exceptions in a type-safe manner.
    • IO: Separates pure code from impure code to improve architecture.
    • Future: Enables writing code without explicit await statements.
    • RequiresContext: Provides a readable, explicit, and type-safe way to inject dependencies.
    • pipelines: Allows creating complex, declarative, and type-safe data pipelines, which can be used independently or with the containers above.
  2. Overview of supported Containers

    master

    The returns library provides several container types to wrap values and maintain execution context. Use these containers to handle specific logic patterns:

    • Maybe: Handles None cases.
    • Result: Handles possible exceptions (Success/Failure).
    • IO: Marks explicit IO actions.
    • Future: Works with async code.
    • RequiresContext: Passes context to functions (e.g., for Dependency Injection).

    Combinations are also available, such as IOResult, FutureResult, RequiresContextResult, RequiresContextIOResult, and RequiresContextFutureResult.

  3. Container Immutability and Type Safety

    master

    Immutability

    Containers in returns are designed to be immutable. You cannot mutate the internal state via __setattr__ or __delattr__, and __slots__ are used to prevent adding new attributes. To make your own classes immutable, you can use the returns.primitives.types.Immutable mixin.

    Type Safety

    returns provides optional type safety via mypy. The library includes PEP 561 compatible .pyi stub files and custom mypy plugins to improve the developer experience and catch type violations during static analysis.

  4. Understand General vs Specific interfaces

    master

    The library provides two tiers of interfaces:

    1. General Interfaces (.interfaces.*): These are agnostic to the returns package and can be applied to any type. They only define standard behaviors (e.g., MappableN only requires a .map method).
    2. Specific Interfaces (.interfaces.specific.*): These are aware of other types within the returns package. They require methods that rely on specific returns types (e.g., ResultLikeN requires a .bind_result method which relies on the Result type).
  5. Supported mypy plugin features

    master

    The returns mypy plugin provides type-safety improvements and new features for the following functional programming patterns:

    • kind: Adds Higher Kinded Types (HKT) support.
    • curry: Allows writing typed curried functions.
    • partial: Allows writing typed partial application.
    • flow: Improves typing for functional pipelines using the flow function.
    • pipe: Improves typing for functional pipelines using the pipe function.
    • do-notation: Enables support for do-notation syntax.
  6. Understand the IO container behavior

    master

    The IO implementation in returns.io is not lazy by design. This ensures that marking a function with @impure behaves as expected in standard Python, with only the return type changing.

    If you require laziness, you can wrap the IO container in a lambda or use unsafe_perform_io to simulate it.

    from returns.io import IO
    
    # Making IO lazy using a lambda
    lazy = lambda: IO(1)
    assert lazy() == IO(1)
  7. Handle generics with `returns.curry.partial`

    master

    Unlike functools.partial, returns.curry.partial correctly handles generic types.

    Workaround for explicit generics: If you pass an explicit generic like [1, 2, 3], mypy may resolve it to List[Any]. To ensure correct type inference, pass annotated variables instead of explicit generic literals.

    from returns.curry import partial
    from typing import List, TypeVar
    
    T = TypeVar('T')
    
    x: List[int]
    
    def some_function(first: List[T], second: int) -> T:
        return first[second]
    
    # Correctly reveals: def (second: int) -> int
    reveal_type(partial(some_function, x))
  8. Handle potential exceptions with the @safe decorator and Result container

    master

    Instead of using try/except blocks to handle exceptions, you can use the @safe decorator. This decorator wraps the return value of a function in a Result container, which will be either a Success[YourType] or a Failure[Exception]. This prevents functions from throwing unexpected exceptions and allows for declarative error handling using flow and bind.

    import requests
    from returns.result import Result, safe
    from returns.pipeline import flow
    from returns.pointfree import bind
    
    
    def fetch_user_profile(user_id: int) -> Result['UserProfile', Exception]:
        """Fetches `UserProfile` TypedDict from foreign API."""
        return flow(
            user_id,
            _make_request,
            bind(_parse_json),
        )
    
    
    @safe
    def _make_request(user_id: int) -> requests.Response:
        # This will return Success[Response] or Failure[Exception]
        response = requests.get('/api/users/{0}'.format(user_id))
        response.raise_for_status()
        return response
    
    
    @safe
    def _parse_json(response: requests.Response) -> 'UserProfile':
        # This will return Success[UserProfile] or Failure[Exception]
        return response.json()
  9. Enable the returns mypy plugin

    master

    To enable the plugin, add returns.contrib.mypy.returns_plugin to your mypy configuration. It is recommended to add this plugin as the first one in your plugin chain.

    For setup.cfg or mypy.ini:

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

    For pyproject.toml:

    [tool.mypy]
    plugins = ["returns.contrib.mypy.returns_plugin"]
  10. Create a custom container

    master

    To create a custom container in returns, follow these steps:

    1. Choose Interfaces: Decide which capabilities your container needs. You can subtype specific interfaces like MappableN, BindableN, AltableN, LashableN, or Equable. You can also use pre-defined aliases like BiMappableN or SwappableN to combine multiple interfaces.
    2. Implement BaseContainer: It is highly recommended to inherit from returns.primitives.container.BaseContainer to gain features like immutability, cloning, serialization, and comparison.
    3. Implement Methods: You must implement all abstract methods required by the interfaces you subtype (e.g., map, bind, lash, alt, swap) to satisfy mypy type checking.
    4. Define Custom Interfaces: If existing interfaces don't cover your needs, define a new interface (e.g., using typing.Protocol) and add it as a supertype.
    5. Verify Laws: Use hypothesis to check that your container adheres to functional programming laws. You can define custom laws using a LawSpec and verify them with check_all_laws.
    6. Write Type-Tests: Use mypy snapshots (e.g., with pytest-mypy-plugins) to ensure your container's type signatures behave correctly under both valid and invalid usage.
    from typing import Callable, TypeVar, Tuple, final
    
    from returns.interfaces import bindable, equable, lashable, swappable
    from returns.primitives.container import BaseContainer
    from returns.primitives.hkt import SupportsKind2
    
    _FirstType = TypeVar('_FirstType')
    _SecondType = TypeVar('_SecondType')
    _NewFirstType = TypeVar('_NewFirstType')
    _NewSecondType = TypeVar('_NewSecondType')
    
    @final
    class Pair(
        BaseContainer,
        SupportsKind2['Pair', _FirstType, _SecondType],
        bindable.Bindable2[_FirstType, _SecondType],
        swappable.Swappable2[_FirstType, _SecondType],
        lashable.Lashable2[_FirstType, _SecondType],
        equable.Equable,
    ):
        def __init__(
            self, inner_value: Tuple[_FirstType, _SecondType],
        ) -> None:
            super().__init__(inner_value)
  11. Use RequiresContext for dependency injection

    master

    The RequiresContext[ReturnType, EnvType] container allows you to implement dependency injection by wrapping a function that accepts an environment/dependency object. Instead of passing parameters through every level of your callstack, you wrap your logic in RequiresContext. Dependencies are injected at the very last moment when the container is called with the environment object.

    Note: RequiresContext and similar types are not recursion safe. Deep nesting exceeding sys.getrecursionlimit() will cause a RecursionError.

    from typing import Protocol
    from returns.context import RequiresContext
    
    class _Deps(Protocol):
        WORD_THRESHOLD: int
    
    def calculate_points(word: str) -> RequiresContext[int, _Deps]:
        guessed_letters_count = len([letter for letter in word if letter != '.'])
        return _award_points_for_letters(guessed_letters_count)
    
    def _award_points_for_letters(guessed: int) -> RequiresContext[int, _Deps]:
        return RequiresContext(
            lambda deps: 0 if guessed < deps.WORD_THRESHOLD else guessed,
        )
    
    # To execute, pass the dependencies at the end:
    # result = calculate_points("abc")(my_deps_instance)