returns Maybe and IOResult railway composition with map and bind: working versions (tested 2026-10-16)
Railway-style composition over returns Maybe and IOResult: map versus bind, short-circuiting Nothing, maybe and bind_optional, impure_safe failures and IO-boxed unwrap, all tested offline.
passed6 passed · 0 failed2026-10-16 09:58 UTC- Status
- passed
- Last passed
- 2026-10-16 09:58 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions3
returns@0.29.0typing-extensions@4.16.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("returns-maybe-ioresult") (install), or see them on GitHub.
requirements.txt
returns==0.29.0 typing-extensions==4.16.0 pytest==9.1.1
maybe_ioresult.py
from returns.io import IOResult, IOSuccess, IOFailure, impure_safe
from returns.maybe import Maybe, Nothing, Some, maybe
from returns.result import Failure, Result, Success
_TAKEN = {3, 7}
@maybe
def parse_seat(raw: str) -> int | None:
return int(raw) if raw.isdigit() else None
def in_range(number: int) -> Maybe[int]:
return Some(number) if 1 <= number <= 9 else Nothing
def maybe_to_result(value: Maybe[int], error: str) -> Result[int, str]:
return value.map(Success).value_or(Failure(error))
@impure_safe
def reserve(number: int) -> str:
if number in _TAKEN:
raise ValueError(f"seat {number} taken")
return f"seat-{number}"
def checkout(raw: str) -> IOResult[str, str]:
seat = parse_seat(raw).bind(in_range)
return IOResult.from_result(maybe_to_result(seat, f"invalid seat: {raw}")).bind(reserve)
tests/test_maybe_ioresult.py
import pytest
from returns.io import IO, IOFailure, IOSuccess
from returns.maybe import Maybe, Nothing, Some
from returns.result import Failure, Success, UnwrapFailedError
import maybe_ioresult as m
def test_some_none_is_not_nothing_but_maybe_normalizes_none():
assert Some(None) != Nothing
assert Maybe.from_optional(None) == Nothing
assert m.parse_seat("x") == Nothing
assert m.parse_seat("4") == Some(4)
assert bool(Nothing) is False
assert bool(Some(0)) is True
def test_map_nests_but_bind_flattens_on_maybe():
assert Some(2).map(lambda n: Some(n + 1)) == Some(Some(3))
assert Some(2).bind(lambda n: Some(n + 1)) == Some(3)
assert Some(2).map(lambda n: Nothing) == Some(Nothing)
assert Some(2).bind(lambda n: Nothing) == Nothing
def test_nothing_short_circuits_and_bind_optional_absorbs_none():
seen = []
def step(value):
seen.append(value)
return Some(value)
assert Nothing.map(step) == Nothing
assert Nothing.bind(step) == Nothing
assert seen == []
assert Some("4").bind_optional(lambda s: int(s) if s.isdigit() else None) == Some(4)
assert Some("x").bind_optional(lambda s: int(s) if s.isdigit() else None) == Nothing
def test_impure_safe_captures_exceptions_and_io_boxes_unwrap():
assert m.reserve(2) == IOSuccess("seat-2")
failed = m.reserve(3)
assert isinstance(failed, IOFailure)
assert failed.failure()._inner_value.args == ("seat 3 taken",)
assert failed.value_or("none") == IO("none")
assert failed.value_or("none") != "none"
with pytest.raises(UnwrapFailedError) as exc:
failed.unwrap()
assert isinstance(exc.value.__cause__, ValueError)
def test_bind_result_bridges_result_while_bind_keeps_the_io_rail():
assert IOSuccess(2).bind_result(lambda n: Success(n + 1)) == IOSuccess(3)
assert IOSuccess(2).bind_result(lambda n: Failure("no")) == IOFailure("no")
assert IOSuccess(2).bind(lambda n: Success(n + 1)) == Success(3)
def test_checkout_combines_maybe_and_ioresult():
assert m.checkout("2") == IOSuccess("seat-2")
assert m.checkout("x") == IOFailure("invalid seat: x")
assert m.checkout("99") == IOFailure("invalid seat: 99")
taken = m.checkout("3")
assert isinstance(taken, IOFailure)
assert isinstance(taken.failure()._inner_value, ValueError)
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.