Slightly alternative way to write the code
Project description
fnkit
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]: aJust/Nothingpair for values that may or may not exist, replacingNone-based optionality with a type-checked container.Result[T, E]: anOk/Errpair for computations that may fail, replacing exception-driven control flow with explicit, inspectable failure values.State[S, A]: a composable wrapper aroundS -> (A, S)functions, for threading state through a pipeline without mutation or global variables.- Monad transformers (
o_bindt,r_bindt): composeStatewithOptionorResultdirectly, so a stateful computation can short-circuit onNothingorErrwithout manual unwrapping at every step. map/bind/liftacross 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, andStateareProtocols, 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, andErrcompare and hash by value, not by identity, since all three are plain frozen dataclasses.Statecompares and hashes by function identity, since two closures cannot be compared for behavioral equality in general. Do not rely onState() == State()for anything other than the same object.liftforOptionandResultevaluates its first argument before its second, so when both sides areNothing/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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20fd1618fb247bf624dc344fcfff215692da1e2e0092a9ef9c829c7041b0285c
|
|
| MD5 |
f9e2ef910acce20e3fd8e66d9501e685
|
|
| BLAKE2b-256 |
80b8ca3bb3237803a772db305bedbdb6fb571f530c1a09a304626d470481207e
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb74b77fedb6187377c63c0fa8d9c9f8219911d40c5429dadd30af1baaefbbb8
|
|
| MD5 |
f30944910eee95bc710266dd22a71140
|
|
| BLAKE2b-256 |
3f9c139adb39d7af614baf7c1524c38856335bc72fb2384e10063eacbd5ff83b
|