Skip to main content

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

CI PyPI Python License: MIT

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.pyOption, factories, fluent combinators, pattern matching.
  • examples/02_either_rop.pyEither ROP pipeline with @safe, map_failure, tap.
  • examples/03_async_pipeline.py — async ROP with map_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

either_option-0.1.1.tar.gz (17.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

either_option-0.1.1-py3-none-any.whl (18.6 kB view details)

Uploaded Python 3

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

Hashes for either_option-0.1.1.tar.gz
Algorithm Hash digest
SHA256 5fe56ff87663526555aa429bdbcdb23619d90fe9a3975c211e329f3ff1dcd4cc
MD5 4b7279752da46ffb8dc2e0169e41c277
BLAKE2b-256 4a8cebd09153bac048b887c038d5e59fb7c7eceaf02a32e9f6ec7153da217b14

See more details on using hashes here.

Provenance

The following attestation bundles were made for either_option-0.1.1.tar.gz:

Publisher: release.yml on baodq97/either-option

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for either_option-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ce759411b683251d0dcafdeaf9b3bfd9a95ca29fcd6d0bbb89aead509ddfe2b2
MD5 2ceb8fc3b6910abecf60db811efb087c
BLAKE2b-256 17b44971526239a97f3d7ed3cc40ad966bb0f566aaa8499a84760cac97b86b54

See more details on using hashes here.

Provenance

The following attestation bundles were made for either_option-0.1.1-py3-none-any.whl:

Publisher: release.yml on baodq97/either-option

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page