dry-python/returns: typed monads for Python functions
Make your functions return something meaningful, typed, and safe!
At a glance
- What is it?
- The library turns Optional, exception-prone and impure code into typed containers such as Maybe, Result and IOResult. It is a real commitment to functional style, and the mypy plugin is not optional in practice.
- Who is it for?
- Adopt it on a codebase that already runs strict mypy and has a boundary layer where Optional, exceptions and IO pile up: start with Maybe and Result in one module. Do not adopt it as a general error-handling replacement across a large team that has not agreed on the style, because every caller has to learn the containers.
- Can I use it commercially?
- Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem dry-python/returns solves
Python signals absence and failure with two things the type checker treats loosely: None and exceptions. A function annotated Optional[User] tells the reader nothing about which branch produced the empty value, and an exception raised three calls deep is invisible in the signature. The README frames this directly, quoting the claim that None is called the worst mistake in the history of Computer Science, and then shows the alternative: a chain of if some is not None checks that grows with every layer.
The library's answer is to make the return type carry the outcome. Maybe is either Some or Nothing. Result is either Success or Failure. IO and IOResult mark impure operations. Future and FutureResult do the same for async code. The audience is Python teams that already annotate heavily and run mypy in CI, and that are willing to write business logic as composition rather than as statements. If your codebase has no type checking, the containers lose most of their value, because the safety they advertise is enforced by the type checker, not at runtime.
How the containers compose: Maybe, Result and bind_optional
The mechanism is the same across the containers. A value is wrapped in a container, and you chain operations with methods such as bind and bind_optional instead of writing control flow. The README's example starts from a plain Optional-returning function, decorates it with @maybe, and then calls bind_optional with a lambda. The documented guarantee is that the lambda is not invoked when the container holds Nothing, so the None checks disappear from the call site without disappearing from the program.
The same shape applies to Result: a function returns Success(value) or Failure(error), and callers compose rather than catch. IO and IOResult separate the description of an impure action from its execution, which the README describes as marking all impure operations and structuring them. RequiresContext covers dependency injection by threading a context through the chain instead of assembling a container object. The README also points to do-notation for writing these chains in a flatter style.
The design is not free. Each container adds a layer between your function and its caller, and the README's own refactored example is longer than the if-chain it replaces. What you buy is that the failure path is in the signature and the type checker can follow it. What you pay is that every reader of that module needs to know what bind_optional does.
Installing dry-python/returns and configuring the mypy plugin
The README gives a single install command, plus a variant that pins a compatible mypy. Python 3.11 or newer is required according to pyproject.toml, and runtime dependencies are limited to typing-extensions.
pip install returnsIf you want the mypy version the project tests against, the README shows the extra:
pip install returns[compatible-mypy]The plugin is the part people skip and then wonder why types do not resolve. The README shows two equivalent configurations. In setup.cfg or mypy.ini:
[mypy]
plugins =
returns.contrib.mypy.returns_pluginOr, in pyproject.toml:
[tool.mypy]
plugins = ["returns.contrib.mypy.returns_plugin"]A first real use is the Maybe decorator from the README. Annotating an Optional-returning function with @maybe converts it to Maybe, and bind_optional then chains only over the present value:
from typing import Optional
from returns.maybe import Maybe, maybe
@maybe
def bad_function() -> Optional[int]: ...
maybe_number: Maybe[float] = bad_function().bind_optional(
lambda number: number / 2,
)After running mypy with the plugin enabled, the annotation on maybe_number should hold, and a call to bind_optional on a Nothing value should not execute the lambda. There is also an optional extras group named check-laws, which pulls in pytest and hypothesis, for projects that want to verify the container laws.
Where dry-python/returns gets in the way
The strongest limitation is that the library is a style, and styles do not merge cleanly. A module written with Result returning Success and Failure is awkward to call from a module that expects exceptions, so the boundary code has to unwrap and re-raise, or the caller has to be converted too. The README does not document a rollback path or an incremental adoption guide for mixed codebases, so the migration cost is something you estimate from your own call graph.
Second, the mypy plugin is load-bearing. Without it, the emulated Higher Kinded Types support that the README advertises does not resolve, and the annotations degrade into something close to Any. A team that runs mypy without the plugin, or that pins a mypy version outside the range declared in pyproject.toml, gets a library that looks typed and is not.
Third, this is a poor fit for scripts and small services. If a function has one caller and one failure mode, wrapping it in a container adds indirection without adding information. The same applies to codebases that lean on framework-level exception handling, where the framework, not your type annotations, decides what happens on failure.
Compared with plain Optional and try/except
The realistic alternative is not another library but the standard tools: Optional with explicit None checks, and try/except with custom exception classes. The difference is where the failure lives. With Optional and exceptions, the failure is in the control flow and in the docstring, and mypy will only catch a missing None check at the point where you dereference the value. With Maybe and Result, the failure is in the return type, and the type checker forces you to handle it before you can reach the inner value.
That shift has a cost the standard approach does not have. Python's own tooling, debuggers and stack traces are built around exceptions, and a Failure value carries no traceback unless you put one in it. When something goes wrong in production, a stack trace from a raised exception is usually faster to read than a container that was never unwrapped. The library is the better choice when the failure is expected and part of the domain, such as validation. Exceptions remain the better choice for programming errors and for failures you cannot handle locally.
Maintenance, licence and upgrade cost
The repository is not archived, and the last push was on 2026-09-21. Releases have been frequent through 2026, with 0.29.0 published on 2026-08-02, 0.28.0 on 2026-06-04 and 0.27.0 on 2026-04-14. The version string in pyproject.toml is 0.29.0, and the classifier still reads Development Status :: 4 - Beta, which is worth knowing before you build a large surface on it.
Upgrade cost is driven by two pinned ranges. The runtime dependency is typing-extensions >=4.0,<5.0, and the optional mypy extra is mypy >=1.19,<2.4. If your project pins mypy outside that window, the compatible-mypy extra will not help you, and you should check whether the plugin still loads before upgrading either side. Python support starts at 3.11, so older interpreters are out.
On licensing, the README badge and the repository metadata do not agree: the LICENSE file is BSD-2-Clause while pyproject.toml declares license = "BSD-3-Clause". Both are permissive and neither imposes copyleft obligations on your code, but the discrepancy is a fact you may want to raise upstream rather than resolve yourself. This is not legal advice; check the LICENSE file in the version you install.
Editorial conclusion
Adopt it on a codebase that already runs strict mypy and has a boundary layer where Optional, exceptions and IO pile up: start with Maybe and Result in one module. Do not adopt it as a general error-handling replacement across a large team that has not agreed on the style, because every caller has to learn the containers. Verify first that your mypy version is inside the supported range and that the plugin loads, since the README shows the plugin configuration but the typing only works when it is active.
Frequently asked questions
How do I install dry-python/returns?
The README gives pip install returns, and pip install returns[compatible-mypy] if you want the mypy version the project supports. Python 3.11 or newer is required according to pyproject.toml.
Do I need the mypy plugin for dry-python/returns?
The README instructs you to configure returns.contrib.mypy.returns_plugin in setup.cfg, mypy.ini or pyproject.toml. The library advertises PEP561 compatibility and emulated Higher Kinded Types, and the plugin is what makes those annotations resolve.
What is the Maybe container in dry-python/returns?
It is a container made of the Some and Nothing types, representing an existing value and an empty state instead of None. The README shows the @maybe decorator converting an Optional-returning function, after which bind_optional runs only when a value is present.
What is the Result container used for in dry-python/returns?
The README describes Result as the container that lets you get rid of exceptions, with Success and Failure as its two cases. Callers compose over it instead of catching, which moves the failure into the return type.
Which Python versions does dry-python/returns support?
The dependency declaration in pyproject.toml sets python = "^3.11", so 3.11 is the floor. The README does not list a maximum version.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dry-python-returns)