- Python 100%
| src/nodies | ||
| tests | ||
| .gitignore | ||
| .python-version | ||
| demo.py | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
nodies
A node-graph execution library for Python. Nodes have typed input and output sockets. Outputs connect to inputs, forming a directed acyclic graph that is executed with automatic parallelism and dead-code elimination.
Prepared graph plans are cached on composite nodes after the first execution and reused until the graph topology changes.
Installation
uv pip install nodies
Or add to your pyproject.toml:
[project]
dependencies = [
"nodies",
]
Usage
Defining nodes
Use Node.from_function for a quick functional style, or subclass Node for reusable types:
import types
from nodies import Node
# Functional style
add = Node.from_function(
lambda a, b: {"result": a + b},
input_names=["a", "b"],
output_names=["result"],
name="add",
)
# Subclass style
class MultiplyNode(Node):
def __init__(self):
super().__init__(name="mul")
self.add_input("a")
self.add_input("b")
self.add_output("result")
def execute(self, **kw):
return {"result": kw["a"] * kw["b"]}
Building and running a graph
Graphs are composite nodes. Wire a graph's outer input to an inner node's input by calling .connect() on the outer socket:
from nodies import Node, Executor
# Build nodes
add = Node.from_function(
lambda a, b: {"result": a + b},
input_names=["a", "b"],
output_names=["result"],
name="add",
)
mul = Node.from_function(
lambda a, b: {"result": a * b},
input_names=["a", "b"],
output_names=["result"],
name="mul",
)
# Create the graph container
graph = Node(name="my_graph")
graph.add_input("x")
graph.add_input("y")
graph.add_input("z")
graph.add_output("out")
graph.inner_nodes = [add, mul]
# Wire: (x + y) * z -> out
graph.inputs.x.connect(add.inputs.a)
graph.inputs.y.connect(add.inputs.b)
add.outputs.result.connect(mul.inputs.a)
graph.inputs.z.connect(mul.inputs.b)
mul.outputs.result.connect(graph.outputs.out)
# Execute
result = Executor().run(graph, x=2, y=3, z=4)
print(result["out"]) # 20 → (2 + 3) * 4
Copy semantics
Output sockets automatically apply the right copy strategy when delivering values to downstream inputs. Mark an input readonly=True to promise you won't mutate the value — this allows safe pass-through aliasing instead of deep-copying:
node.add_input("data", readonly=True) # alias (no copy if safe)
node.add_input("data", readonly=False) # may mutate → triggers deepcopy
The rules:
- 1 total consumer → always pass through (no copy)
- All consumers are read-only → pass through (safe aliasing)
- Mixed / multiple RW consumers:
- All RO consumers share one
deepcopy - Each RW consumer except the last receives a
deepcopy - The last RW consumer receives the original (no wasted copy)
- All RO consumers share one
Side-effect nodes
Set pure=False on a node to ensure it always runs, even if its outputs aren't consumed:
logger = Node(name="logger", pure=False)
Development
# Clone the repository
git clone https://git.trainraider.win/trainraider/nodies.git
cd nodies
# Install dependencies
uv sync --all-extras
# Run tests
uv run pytest
# Linting
uv run ruff check src/nodies
License
MIT