Skip to main content

Slightly alternative way to write the code

Project description

fnkit

PyPI version Python versions License: MIT Tests

Typed functional programming primitives for Python 3.14, built on native PEP 695 generics.

Option, Result, and State give you composable, type-checked alternatives to None checks, exception-driven error handling, and hand-rolled state threading, without a class hierarchy to inherit from and without losing type information along the way.

Why this exists

Most Python functional-programming libraries predate PEP 695 and still lean on TypeVar and Generic[T]. This library is built from the ground up on Python 3.14's native generic syntax (class Foo[T], def bar[T, E = Default]), structural Protocols instead of inheritance, and match statements for unwrapping values.

Features

  • Option[T]: a Just / Nothing pair for values that may or may not exist, replacing None-based optionality with a type-checked container.
  • Result[T, E]: an Ok / Err pair for computations that may fail, replacing exception-driven control flow with explicit, inspectable failure values.
  • State[S, A]: a composable wrapper around S -> (A, S) functions, for threading state through a pipeline without mutation or global variables.
  • Monad transformers (o_bindt, r_bindt): compose State with Option or Result directly, so a stateful computation can short-circuit on Nothing or Err without manual unwrapping at every step.
  • map / bind / lift across all three types, for a consistent functional interface regardless of which container you are working with.
  • Fully immutable: every type is a frozen, slotted dataclass. No shared mutable state, no reference cycles.
  • Structural typing throughout: Option, Result, and State are Protocols, so you can write your own conforming implementations without subclassing.

Requirements

Python 3.14 or later.

Installation

pip install fnkit

Quick examples

Option

from fnkit.option import Just, Nothing, Option, pure

def find_user(name: str) -> Option[str]:
    return Just(name) if name == "ada" else Nothing()

result = (
    find_user("ada")
    .map(str.upper)
    .bind(lambda name: Just(f"hello, {name}"))
    .just_or(lambda: "user not found")
)
print(result)  # hello, ADA

Result

from fnkit.result import Err, Ok, Result

def parse_positive(raw: str) -> Result[int, str]:
    value = int(raw)
    return Ok(value) if value > 0 else Err(f"{value} is not positive")

result = (
    parse_positive("42")
    .map(lambda n: n * 2)
    .map_err(lambda e: f"parse failed: {e}")
    .ok_or(lambda e: -1)
)
print(result)  # 84

State

from fnkit.state import State, pure

def push[T](item: T) -> State[list[T], None]:
    def run(stack: list[T]) -> tuple[None, list[T]]:
        return None, [*stack, item]
    return State(run)

def pop[T]() -> State[list[T], T]:
    def run(stack: list[T]) -> tuple[T, list[T]]:
        return stack[-1], stack[:-1]
    return State(run)

program = push(1).bind(lambda _: push(2)).bind(lambda _: pop())
value, final_state = program.run_state([])
print(value, final_state)  # 2 [1]

Combining State with Result or Option

from fnkit.result import Err, Ok, Result
from fnkit.state import State
from fnkit.transformers import r_bindt

def read_and_validate(state: State[int, Result[int, str]]) -> State[int, Result[int, str]]:
    def validate(value: int) -> State[int, Result[int, str]]:
        if value < 0:
            return State(lambda s: (Err("negative value"), s))
        return State(lambda s: (Ok(value * 2), s))

    return r_bindt(state, validate)

Design notes

  • Nothing, Ok, and Err compare and hash by value, not by identity, since all three are plain frozen dataclasses.
  • State compares and hashes by function identity, since two closures cannot be compared for behavioral equality in general. Do not rely on State() == State() for anything other than the same object.
  • lift for Option and Result evaluates its first argument before its second, so when both sides are Nothing/Err, the first argument's failure is the one that propagates. This matches left-to-right short-circuiting.

License

MIT. See LICENSE for the full text.

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

fnkit-0.5.0.tar.gz (4.5 kB view details)

Uploaded Source

Built Distribution

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

fnkit-0.5.0-py3-none-any.whl (5.8 kB view details)

Uploaded Python 3

File details

Details for the file fnkit-0.5.0.tar.gz.

File metadata

  • Download URL: fnkit-0.5.0.tar.gz
  • Upload date:
  • Size: 4.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fnkit-0.5.0.tar.gz
Algorithm Hash digest
SHA256 20fd1618fb247bf624dc344fcfff215692da1e2e0092a9ef9c829c7041b0285c
MD5 f9e2ef910acce20e3fd8e66d9501e685
BLAKE2b-256 80b8ca3bb3237803a772db305bedbdb6fb571f530c1a09a304626d470481207e

See more details on using hashes here.

File details

Details for the file fnkit-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: fnkit-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 5.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fnkit-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cb74b77fedb6187377c63c0fa8d9c9f8219911d40c5429dadd30af1baaefbbb8
MD5 f30944910eee95bc710266dd22a71140
BLAKE2b-256 3f9c139adb39d7af614baf7c1524c38856335bc72fb2384e10063eacbd5ff83b

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