fnloom¶
Write plain Python functions. Get a fast, strict, deterministic dataflow graph. fnloom uses the Apache Hamilton (incubating) model you may already know, with the planner and executor rebuilt in Rust.
-
Your functions are the graph
A function's name is a node, and its parameter names are the edges. No decorators, no registration, no config objects. Your code stays plain Python you can call and test on its own.
-
Up to 24× faster repeat runs on plain graphs
Compile a request once with
prepare, then run it as often as you like. A 1,000-node graph runs in about 0.2 ms, where Hamilton takes over 4 ms. -
Mistakes fail loudly, and early
A misspelled input, a cycle, a type mismatch or an
async defnode in a sync build raises a clear error naming the culprit, before any node runs. Hamilton accepts several of these silently. -
The same order, every run
Nodes run in topological order with ties broken by name, however you list the outputs. Logs and side effects never shuffle between runs.
-
No depth limit
Nothing recurses over the graph, so a 50,000-node chain builds and runs. Hamilton hits
RecursionErrorat around 1,000. -
A Rust core you can embed
fnloom-corehas no dependencies and nounsafe. Its graph, planner and executor don't care what a node runs.
A graph in one screen¶
Start with a module of plain functions:
# file: pricing.py
def total(price: float, quantity: int) -> float:
"""What the line costs before any discount."""
return price * quantity
def discounted(total: float, discount: float = 0.0) -> float:
"""The total after the discount, if one is given."""
return total * (1 - discount)
def label(discounted: float, currency: str = "EUR") -> str:
"""The price as shown to a customer."""
return f"{discounted:.2f} {currency}"
fnloom reads this graph from it. price and quantity aren't the name of any function, so they're inputs. discount
and currency have defaults, so they're optional (dashed):
flowchart TD
price([price]) --> total
quantity([quantity]) --> total
total --> discounted
discount([discount?]) -.-> discounted
discounted --> label
currency([currency?]) -.-> label
Build a driver and ask for what you want. Only the functions your outputs need will run:
import fnloom
import pricing
driver = fnloom.build(pricing)
driver.execute(["label"], inputs={"price": 2.5, "quantity": 4})
# -> {'label': '10.00 EUR'}
driver.execute(["label"], inputs={"price": 2.5, "quantity": 4, "discount": 0.1})
# -> {'label': '9.00 EUR'}
Why not just use Hamilton?¶
fnloom keeps what makes Hamilton pleasant and fixes what bites. A few highlights:
| Hamilton 1.90.0 | fnloom | |
|---|---|---|
| A chain of 1,000+ nodes | RecursionError |
Runs at any depth |
| A dependency cycle | RecursionError when run |
ValueError naming the cycle |
| A misspelled input | Silently ignored | ValueError naming it |
| Execution order | Depends on how you list outputs | Always the same |
| Running a request again | Re-planned every call | prepare once, run many times |
Every difference is deliberate and listed in the register of differences, and most are tested against Hamilton side by side.
Status: pre-alpha
fnloom isn't on PyPI yet, so build it from source. The Rust core, the native Python path,
@parameterize, @tag, @config.when*, @extract_fields, @extract_columns, @exclude, graph inspection,
validate, graphs from plain functions, namespaces, type checks, result caching, the async driver,
hooks, result builders, output checks, custom value checks, drawing, export and the
CLI, @check_output validators, steps,
@inject, @resolve, node wrappers and bind= for namespaces
are done. Dynamic execution comes later;
the design has the full plan.