Railway-Oriented Programming for Python — Option[T] and Either[T, E] sum types with sync + async combinators, sealed pattern matching, and 100% branch coverage.
Project description
either-option
Railway-Oriented Programming for Python. A typed, sealed Option[T] and
Either[T, E] with sync and async combinators. Pyright --strict clean,
100% line + branch coverage, 545 tests.
A Python port of the C# Optional library
by Nils Lück, reframed for ROP in idiomatic Python 3.10+.
Why this library
There are several Optional-flavoured packages on PyPI. either-option is the
only one that ships all of:
| Feature | either-option |
py-optional |
optional-python (ponytailer) |
optional (alex, 2015) |
|---|---|---|---|---|
Option[T] (Some / Nothing) |
✓ | ✓ | ✓ | ✓ |
Either[T, E] (Success / Failure) |
✓ | — | partial (Try) |
— |
async combinators (map_async, flat_map_async, …) |
✓ | — | — | — |
Sealed match / pattern matching |
✓ | — | — | — |
@safe / @safe_async decorators |
✓ | — | — | — |
Collection helpers (first_or_none, successes, …) |
✓ | — | — | — |
Pyright --strict clean |
✓ | mypy | — | — |
| 100% line + branch coverage | ✓ | partial | — | — |
| Active | ✓ | ✓ | abandoned | abandoned |
Install
pip install either-option
# or
uv add either-option
Requires Python 3.10+ (tested on 3.10–3.14).
Quick start
from either_option import Either, Failure, Success
from either_option.safe import safe
@safe(catch=ValueError)
def parse_age(raw: str) -> int:
return int(raw)
def validate(age: int) -> Either[int, str]:
return Either.some(age) if 0 <= age <= 130 else Either.none("out of range")
result = (
parse_age("42")
.map_failure(lambda e: f"parse: {e}")
.flat_map(validate)
)
match result:
case Success(age): print(f"got {age}")
case Failure(err): print(f"oops: {err}")
case _: pass # Either is sealed
What's in the box
from either_option import (
Option, Some, Nothing, some, nothing, # presence/absence
Either, Success, Failure, flatten, # success/failure + flatten()
OptionValueMissingError, # raised by opt-in unsafe getters
)
from either_option.safe import safe, safe_async, call_safe
from either_option.extensions import some_not_none, some_when, none_when, from_optional
from either_option.collections import (
first_or_none, last_or_none, single_or_none, element_at_or_none, get_or_none,
values, successes, failures,
)
from either_option.unsafe import value_or_failure, value_or_default, to_optional
Sync combinators: map, flat_map, filter, tap, match, value_or,
value_or_else, value_or_with, or_else, or_with, or_option_else,
or_option_with, map_failure, flat_map_failure, to_iterable, contains,
exists.
Async variants: every method above has an _async counterpart accepting
async callables (map_async, flat_map_async, filter_async,
value_or_else_async, or_with_async, …), plus
Either.from_awaitable(awaitable, catch=...) to lift coroutines.
Examples
Runnable end-to-end demos in examples/:
examples/01_option_basics.py—Option, factories, fluent combinators, pattern matching.examples/02_either_rop.py—EitherROP pipeline with@safe,map_failure,tap.examples/03_async_pipeline.py— async ROP withmap_async,flat_map_async,Either.from_awaitable.examples/04_collections_and_unsafe.py— collection helpers + opt-in unsafe extraction.
uv run python examples/01_option_basics.py
uv run python examples/02_either_rop.py
uv run python examples/03_async_pipeline.py
uv run python examples/04_collections_and_unsafe.py
Development
Prerequisites
| Tool | Min version | Why | Install |
|---|---|---|---|
| Python | 3.10 | Library targets >=3.10. uv installs the right interpreter for you on first sync. |
See uv below — it installs Python 3.10 automatically. |
| uv | 0.11 | Package manager, build backend (uv_build), test runner, and Python installer. The repo pins required-version = ">=0.11" so older versions are rejected. |
Win: powershell -ExecutionPolicy Bypass -c "irm https://astral.sh/uv/install.ps1 | iex"macOS / Linux: curl -LsSf https://astral.sh/uv/install.sh | sh |
| Git | any recent | Clone repo + (optionally) reference upstream C# source. | Platform package manager. |
First-time setup
git clone https://github.com/baodq97/either-option
cd either-option
uv sync # installs Python 3.10, dev deps (ruff, pyright, pytest)
uv run pytest # verify smoke test passes
The verify loop
These commands are the contract the codebase must always satisfy:
uv run ruff format --check . # formatter
uv run ruff check . # linter (rules: ALL minus formatter conflicts)
uv run pyright # type checker (strict, every report* = error)
uv run pytest -q # tests (filterwarnings = error)
uv run pytest --cov # tests + coverage (requires 100%)
To apply instead of just checking:
uv run ruff check --fix . # apply auto-fixable lint
uv run ruff format . # apply formatter
Multi-version testing
uv python install 3.10 3.11 3.12 3.13 3.14 # ~150 MB per version, one-time
uv run --python 3.11 pytest # run tests on Python 3.11
uv run --python 3.12 pyright # type-check on Python 3.12
Repo layout
either-option/
├── src/either_option/ # shipped Python code
│ ├── __init__.py # public re-exports
│ ├── _core.py # Option/Either + concrete subclasses, sync + async
│ ├── extensions.py # some_not_none / some_when / none_when / from_optional
│ ├── collections.py # first_or_none / values / successes / failures …
│ ├── unsafe.py # value_or_failure / value_or_default / to_optional
│ ├── safe.py # @safe / @safe_async / call_safe
│ └── py.typed # PEP 561 marker
├── tests/ # pytest suite (545 tests; 100% line+branch coverage)
├── examples/ # runnable demo scripts
├── reference/ # gitignored: clone of nlkl/Optional (C# source) for porting reference
├── pyproject.toml # single source of truth: deps, ruff, pyright, pytest, uv config
├── CLAUDE.md # rules for AI agents working in this repo
└── .python-version # 3.10 — pins dev interpreter to the support floor
The reference/ folder is gitignored — clone the upstream C# source there with:
git clone --depth 1 https://github.com/nlkl/Optional reference/optional
It is purely for reading; nothing under reference/ is built, tested, or shipped.
Contributing
PRs welcome. Read CONTRIBUTING.md for the dev setup,
verify loop, and PR flow. Security issues: see SECURITY.md.
Changelog
See CHANGELOG.md.
Credits
This project is a Python port of the C# Optional
library by Nils Lück, distributed under the MIT License. The original copyright
notice is preserved in LICENSE.
License
MIT © 2026 Bao Do.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file either_option-0.1.1.tar.gz.
File metadata
- Download URL: either_option-0.1.1.tar.gz
- Upload date:
- Size: 17.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5fe56ff87663526555aa429bdbcdb23619d90fe9a3975c211e329f3ff1dcd4cc
|
|
| MD5 |
4b7279752da46ffb8dc2e0169e41c277
|
|
| BLAKE2b-256 |
4a8cebd09153bac048b887c038d5e59fb7c7eceaf02a32e9f6ec7153da217b14
|
Provenance
The following attestation bundles were made for either_option-0.1.1.tar.gz:
Publisher:
release.yml on baodq97/either-option
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
either_option-0.1.1.tar.gz -
Subject digest:
5fe56ff87663526555aa429bdbcdb23619d90fe9a3975c211e329f3ff1dcd4cc - Sigstore transparency entry: 1386971275
- Sigstore integration time:
-
Permalink:
baodq97/either-option@cf2dca4cc7465a44c7ab898732e9b5e7ab127cbe -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/baodq97
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cf2dca4cc7465a44c7ab898732e9b5e7ab127cbe -
Trigger Event:
push
-
Statement type:
File details
Details for the file either_option-0.1.1-py3-none-any.whl.
File metadata
- Download URL: either_option-0.1.1-py3-none-any.whl
- Upload date:
- Size: 18.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce759411b683251d0dcafdeaf9b3bfd9a95ca29fcd6d0bbb89aead509ddfe2b2
|
|
| MD5 |
2ceb8fc3b6910abecf60db811efb087c
|
|
| BLAKE2b-256 |
17b44971526239a97f3d7ed3cc40ad966bb0f566aaa8499a84760cac97b86b54
|
Provenance
The following attestation bundles were made for either_option-0.1.1-py3-none-any.whl:
Publisher:
release.yml on baodq97/either-option
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
either_option-0.1.1-py3-none-any.whl -
Subject digest:
ce759411b683251d0dcafdeaf9b3bfd9a95ca29fcd6d0bbb89aead509ddfe2b2 - Sigstore transparency entry: 1386971365
- Sigstore integration time:
-
Permalink:
baodq97/either-option@cf2dca4cc7465a44c7ab898732e9b5e7ab127cbe -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/baodq97
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cf2dca4cc7465a44c7ab898732e9b5e7ab127cbe -
Trigger Event:
push
-
Statement type: