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.


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.0.tar.gz (17.5 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.0-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

Details for the file either_option-0.1.0.tar.gz.

File metadata

  • Download URL: either_option-0.1.0.tar.gz
  • Upload date:
  • Size: 17.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for either_option-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2b7e97b51656d8e3a15a12ba0dc380b864a0a1b0c35741b62c23357a65c4dd36
MD5 ef598fa14403e1be901865ffa25cfa21
BLAKE2b-256 0ebe115459aab45811bdf91d5e1de9f0e0d063117d741ff8312c77d030abd1a9

See more details on using hashes here.

File details

Details for the file either_option-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: either_option-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for either_option-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 68309448439bb44491fb996e36e324f625c7044a1605ee4b6655a7951a00f022
MD5 725f7f36224d707c3d2dbf16fc2fc1a7
BLAKE2b-256 e472efde55aa869d0b76dd29d45b184711f924084f61d69fae2b7943cacdea04

See more details on using hashes here.

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