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.
Metadata
Release files for either-option 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| either_option-0.1.1.tar.gz | 17.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| either_option-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 36.2 kB
Release files / either_option-0.1.1.tar.gz
| Download URL | either_option-0.1.1.tar.gz |
|---|---|
| Size | 17.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5fe56ff87663526555aa429bdbcdb23619d90fe9a3975c211e329f3ff1dcd4cc
|
|
BLAKE2b-256 checksum How to use checksums |
4a8cebd09153bac048b887c038d5e59fb7c7eceaf02a32e9f6ec7153da217b14
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 26, 2026.
Transparency logRelease files / either_option-0.1.1-py3-none-any.whl
| Download URL | either_option-0.1.1-py3-none-any.whl |
|---|---|
| Size | 18.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ce759411b683251d0dcafdeaf9b3bfd9a95ca29fcd6d0bbb89aead509ddfe2b2
|
|
BLAKE2b-256 checksum How to use checksums |
17b44971526239a97f3d7ed3cc40ad966bb0f566aaa8499a84760cac97b86b54
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 26, 2026.
Transparency log