Future Paths and wend¶
wend grew out of Future Paths, five essays written in January 2026 on what Python paths could keep track of beyond a concrete value. This page maps each essay to the library as it is now. Using wend does not require reading them.
The essays share one distinction, stated in the first: describing a path is different from realising it as a concrete value. Most of what they propose, and most of what wend does, follows from keeping the description.
Part 1: Semantic types¶
The essay asks for paths that carry what the programmer means by them: that a path must exist or must not, is a file or a directory, is a fragment to be placed under a root, or must stay inside a directory. It then sketches lenses, lineage, content addressing, directory schemas, schema inference, an algebra of path sets, late binding and virtual filesystems.
Its fragment and late-binding sketches work as written, with wend's names:
>>> from wend import P, Param
>>> leaf = P("bar")
>>> [p.render() for p in map(leaf.under, ["foo1", "foo2"])]
['foo1/bar', 'foo2/bar']
>>> ROOT = Param("root")
>>> CONFIG = ROOT / "config.yaml"
>>> CONFIG.resolve(root="/actual/root")
PosixPath('/actual/root/config.yaml')
| the essay's sketch | in wend |
|---|---|
Path.fragment(...), leaf.under(parent) |
an expression and under |
PathVar("root"), CONFIG.collect(paths) |
Param, resolve, and bindings from objects |
FilePath, DirectoryPath, NewPath |
the conditions File, Dir, Exists and Absent |
Path[Readable], Path[Writable, MayCreate] |
Readable, Writable, Creatable and Listable |
Path[Under[STATIC_ROOT]] |
Under, and segment parameters, which cannot traverse |
| pre- and postconditions on pipeline steps | requires, ensures and Step |
PathLens(forward=..., backward=...) |
Lens, built from two expressions instead of two functions |
.tag(...), provenance, trace_lineage() |
tag, explain and Ledger |
| content-addressed paths, as in Nix | content_key |
@directory_schema class MLProject |
wend.schema.Tree, with validate and scaffold |
| schema inference, after genson | wend.infer.infer and from_inferred |
a union of UserConfig, SystemConfig and DefaultConfig, tried in order; &, - and PathQ |
unions with first, and queries |
XDG.CACHE_HOME |
wend.anchors |
| fsspec and other filesystems | wend.fs.FsspecFS, for glob and infer |
What changed on the way.
- The capabilities are runtime checks, not types.
Path[Readable]in a signature is something a type checker could verify. wend's conditions are declared on an expression and checked bycheckandStepwhen the program runs; a type checker sees aPathExpr. The conditions journal entry records what static checking could add. - The lens is derived, not written. The essay's lens takes a forward and a backward function. wend's takes two expressions, and derives both directions by matching one and rendering the other, so the two directions cannot disagree.
- The schema is a class of entries, with cardinality per directory and a report of every difference at once, rather than typed attribute access alone.
Part 2: Connectivity¶
The essay observes that after
config = project_root / "config.yaml", the fact that config was built from
project_root is gone. It asks for a relation type, base tracking, rebasing, sibling and
cousin relations, composition rules, and some integration with types.
All but the last are in wend (Relations):
>>> Root = Param("Root")
>>> csv_in = Root / "data" / "input.csv"
>>> schema = csv_in.sibling("schema.json")
>>> schema.render(Root="/tmp/proj")
'/tmp/proj/data/schema.json'
>>> moved = schema.rebase(csv_in, P("/archive/input.csv"))
>>> moved.render()
'/archive/schema.json'
Composition preserves the chain: a path derived from a derived path keeps both steps, so
rebasing any path in the chain moves everything derived from it. Type-level connectivity
(config: Path[Under[root]]) needs dependent types, which Python does not have; Under
is checked at runtime instead, lexically by render and with symlinks resolved by
check.
Part 3: Deferred path-ification¶
The essay builds a proof of
concept on Python 3.14's t-strings: parameters stay symbolic inside a template, joins
build a tree, and resolve substitutes values, with parent, with_suffix and
with_name working before any value is known. wend keeps all of it, and the essay's
examples run as written:
>>> from pathlib import Path
>>> from wend import T
>>> root, idx, total = Param("root"), Param("idx"), Param("total")
>>> chunk_file_t = root / "chunks" / T(t"chunk_{idx:03}-of-{total:03}.parquet")
>>> chunk_file_t.resolve({"root": Path("/data"), "idx": 1, "total": 10})
PosixPath('/data/chunks/chunk_001-of-010.parquet')
>>> chunk_file_t.parent.resolve({"root": Path("/data")})
PosixPath('/data/chunks')
>>> chunk_file_t.with_suffix(".json").resolve(
... {"root": Path("/data"), "idx": 5, "total": 10}
... )
PosixPath('/data/chunks/chunk_005-of-010.json')
What changed is the structure underneath. The proof of concept was a tree of join nodes. wend keeps every expression in a normal form, a head followed by segments, so the number of segments is known before binding. That one change is what makes the rest sound:
- an operation is computed from the structure only when the structure decides it, and is deferred to pathlib otherwise (Operations);
- a value bound to a segment parameter cannot add segments, so it cannot traverse (Parameters);
- an expression can be matched back against a path (Rendering and matching). None of the essays has matching; it came with the normal form.
Part 4: Compilation theory¶
The essay frames path expressions as term rewriting, with three rules: parent–join cancellation, constant folding and suffix-chain collapse. It then sketches a register machine for executing them, and concludes that Python, not a parser, should build the expression tree.
The conclusion holds: the expression tree is built by Python's own operators. The three rules hold too, with the condition the essay did not state. Parent–join cancellation is valid only when the right operand is exactly one segment, so wend applies it only then:
>>> from wend import SubPath
>>> (Root / "a/b").parent
PathExpr('{Root}/a')
>>> (Root / SubPath("rel")).parent
PathExpr('parent({Root}/{rel...})')
wend 0.1 applied the rule by the type of a node rather than its number of segments, and
gave /x for (root / "a/b").parent where pathlib gives /x/a. The
retrospective
records this. The current rules are not a rewrite system applied afterwards: they are what
the normal form computes when an expression is built, and the
contract states the result
they must agree with, which the tests check against an eager pathlib oracle.
The register machine was not built. Its main benefit, computing a shared sub-path once,
appears as compile_many, which looks up, checks and formats each
parameter once for a whole set of expressions.
Part 5: Lazy execution¶
The essay describes a Rust
implementation behind pyo3: an arena of nodes, an optimiser, and a slot-based executor,
with compile_path(expr).resolve(...) from Python.
wend has no Rust backend. Its goal, compiling once and resolving many times without walking the tree, is met in Python:
compilegenerates a specialised Python function per expression, with validation and formatting inlined;compile().manyandcompile().columnsrender in bulk;wend.frame.resolve_framerenders whole Polars columns.
The essay's API also differs: its T("chunk_{idx}.parquet", ["idx"]) took formatting
out of the template and had the caller format each value, which wend's t-string templates
keep in place. Whether a native backend is worth building is open, to be decided on
measurements: issue #37 tracks it, and the
journal records what a backend
would need to beat.
What is still open¶
- Static checking. Capabilities, containment and connectivity as types that a type checker verifies, rather than conditions checked at runtime.
- A native backend, only where measurements justify one (#37).
- Effects as data. Parts 2 and 5 suggest reporting every filesystem effect a program
can have.
Step.effects()lists what each path of a step declares, but nothing collects them across a program.
Project history lists the journal entries that record how each part was built, including the trajectory entry, which tells the same story in more detail.