Every time you turn a structure into outputs — validate it, document it, compile it to SQL, lint
it — you hand-write the same machinery: an if isinstance(...) ladder, a visitor class, a registry
keyed by type. It is bespoke each time. It does not compose — you can't add a case from outside, or
run two passes in one traversal, without rewiring it. And the type checker can't see through the
dispatch, so a missing case is a runtime surprise, not a red squiggle.
A stateless coding agent makes it sharper: it re-derives that machinery from a few thousand lines of context, gets one case wrong, and the dispatch hides the gap.
capability is the handful of primitives that machinery reduces to: a fold over
self-describing values, typed end to end. The values carry their own behaviour; a fold runs
them; and they compose — a new value, a new pass, a new target are each an addition, never an edit
to the core.
uv add capability # zero dependencies, stdlib only
The primitives
A capability is a frozen value that carries its own contribution to a typed context. A fold
threads a context through a sequence of them, dispatching to each by an isinstance check against a
protocol — a value that doesn't speak a target's protocol is skipped. Three targets here: a
request-time validator, a database column, a block of help text.
from dataclasses import dataclass, replace
from typing import Protocol, runtime_checkable
from capability import Phase, by_protocol
@dataclass(frozen=True, slots=True)
class Rule: # a request-time validator
max_len: int | None = None
@runtime_checkable
class Validated(Protocol):
def validate(self, ctx: Rule) -> Rule: ...
@dataclass(frozen=True, slots=True)
class Ddl: # a database column
column: str = "TEXT"
@runtime_checkable
class Stored(Protocol):
def ddl(self, ctx: Ddl) -> Ddl: ...
@dataclass(frozen=True, slots=True)
class Doc: # help lines, accumulated
lines: tuple[str, ...] = ()
@runtime_checkable
class Documented(Protocol):
def doc(self, ctx: Doc) -> Doc: ...
# Each interpretation reads the context the previous fact left, and extends it.
@dataclass(frozen=True, slots=True)
class MaxLen:
n: int
def validate(self, ctx: Rule) -> Rule:
return replace(ctx, max_len=self.n)
def ddl(self, ctx: Ddl) -> Ddl:
return replace(ctx, column=f"VARCHAR({self.n})")
def doc(self, ctx: Doc) -> Doc:
return replace(ctx, lines=(*ctx.lines, f"at most {self.n} characters"))
@dataclass(frozen=True, slots=True)
class Unique:
def ddl(self, ctx: Ddl) -> Ddl:
return replace(ctx, column=f"{ctx.column} UNIQUE")
def doc(self, ctx: Doc) -> Doc:
return replace(ctx, lines=(*ctx.lines, "must be unique"))
validate = Phase(Rule, by_protocol(Validated, lambda c, ctx: c.validate(ctx)))
store = Phase(Ddl, by_protocol(Stored, lambda c, ctx: c.ddl(ctx)))
document = Phase(Doc, by_protocol(Documented, lambda c, ctx: c.doc(ctx)))
field = (MaxLen(255), Unique())
validate.run(field) # Rule(max_len=255) — Unique has no validate, skipped
store.run(field) # Ddl(column='VARCHAR(255) UNIQUE') — both facts fold in
document.run(field) # Doc(lines=('at most 255 characters', 'must be unique'))
Add a fact (MinLen, Indexed): it implements the protocols it has, the rest skip it. Add a
target: a new Phase, no fact changes. Every piece is a small typed value, and nothing in the core
enumerates them.
It does not remove the work — MaxLen still spells out validate, ddl, doc by hand. It removes
the machinery: no visitor, no registry, no dispatch ladder — and the typed apply keeps each call
honest under pyright --strict.
Compose them
Phases compose into a Compiler — a keyed set with set-like operators that folds every phase over
the facts in a single pass:
from capability import Compiler
schema = Compiler((validate, store, document)) # a composable set of phases
bundle = schema.run(field) # one traversal, every phase at once
bundle.get(store) # Ddl(column='VARCHAR(255) UNIQUE')
bundle.get(document) # Doc(lines=('at most 255 characters', 'must be unique'))
# compilers compose like sets: a + b (union) a - b (restrict) a & b (intersect) a | b (override)
The operators form an idempotent semilattice (A + A == A), so composing compilers is predictable;
running one fuses every phase into a single traversal (banana-split). Composition is the point —
primitives into phases, phases into compilers, compilers into each other. Bundle.get is typed: it
hands back exactly the phase's own context type.
The whole engine
The core primitive, with the typing stripped, is a left fold:
def fold(items, initial, step):
ctx = initial
for item in items:
ctx = step(item, ctx)
return ctx
fold names no target — the meaning is all in step, which you build with by_protocol /
by_table (you don't hand fold a bare lambda; a Step carries the policy and lets fold pick
the sync or async path). A new target is a new step, not an edit. The package is nine small
files, ~370 lines, zero dependencies.
The contexts, protocols, and targets are yours. The core ships fold and the dispatch
policies; what they compute to is your code.
Layers
Everything past fold + a Step is a layer you can ignore until you want it.
| Import | What it gives you |
|---|---|
fold, Step |
the primitive: a typed left fold + the dispatch-policy wrapper |
by_protocol |
open on the data axis — a new fact that implements the protocol is picked up |
by_table |
open on the interpreter axis — dispatch by exact type, a per-target handler table |
chain |
run several dispatch policies in one pass |
Phase |
a reified target — a context factory + its step; Phase.run(items) |
Compiler, Bundle |
compose phases (+ - & |) and run them in one pass; Bundle.get(phase) returns that phase's typed context |
fold_tree |
the catamorphism for recursive (tree) descriptions |
rfold, scan |
a right fold; a left scan that keeps every intermediate context |
traced_fold, explain |
record each step and render it — free, because the data is immutable |
from_annotated, from_sidecar |
read facts off Annotated[T, ...] metadata or a class sidecar |
afold |
the async overload — an async def apply makes fold return a coroutine |
capability.reflect.by_method |
opt-in name-driven dispatch |
You've met this before
capability is not a new idea — it's a known one, generalised and kept as small typed data:
functools.singledispatchregisters handlers externally, one function at a time, away from the data. Here the interpretations live on the value, and onefoldruns many at once.- Pydantic's
Annotatedmetadata is this move for a single target — validation.capabilityis the same, generalised to arbitrary phases with an algebra to compose them;from_annotatedreads exactly that metadata. - Object algebras / tagless-final are the typed-FP relatives. This is the data encoding, so
the program stays inspectable — you can print it, diff it,
explainit — which a closure encoding can't.
Correctness
- zero dependencies, stdlib only;
src/ispyright --strictclean; 100% branch coverage; theCompileralgebra and the fold laws are property-tested;- no
getattr/hasattrin the dispatch code. To be exact about whatby_protocoldoes at runtime:isinstanceagainst a@runtime_checkableProtocol, which matches on method-name presence, not signatures — the typedlambda c, ctx: c.validate(ctx)is what buys the call-site check. Name-driven dispatch is opt-in, incapability.reflect.
Lineage
A capability is Reynolds' (1972) defunctionalised closure — a decision turned from code into an
inspectable record. The hot path is a left fold; the catamorphism it instances (Meijer–Fokkinga–
Paterson 1991, after Malcolm 1990) is fold_tree / rfold, for recursive shapes. And it keeps
Wadler's Expression Problem open on both axes.
Small, typed, composable — the rest is yours.
Made with 🧬 by @prostomarkeloff
Metadata
Release files for capability 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| capability-1.0.0.tar.gz | 17.5 kB | Without provenance | Details for capability-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| capability-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for capability-1.0.0-py3-none-any.whl |
Total release size: 34.0 kB
Release files / capability-1.0.0.tar.gz
| Download URL | capability-1.0.0.tar.gz |
|---|---|
| Size | 17.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e1c89ae5a17207d1102f98a5249a3961648e27a1701827e950ab17b163de6bcf
|
|
BLAKE2b-256 checksum How to use checksums |
fafc44f1d59179cf9ed2fe0a62b19ddfbc8732c5c118008266cb7828c1dceafa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.1
|
Release files / capability-1.0.0-py3-none-any.whl
| Download URL | capability-1.0.0-py3-none-any.whl |
|---|---|
| Size | 16.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
853f27fd678f18d034744ef0eac2944df66d90f173ad1cb3be46253291c02b4c
|
|
BLAKE2b-256 checksum How to use checksums |
76dd5edd27ca4afd32e0e80097aa155789ae516cae2922d6ea7eea166050ba73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.1
|
Security for capability 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.