How it works
pipescript is a syntax augmentation of Python: everything you write ends up running as ordinary Python. Understanding the two-stage transform below explains both what pipescript can do and why it composes so cleanly.
Two stages
Stage 1 – token rewriting. pipescript first rewrites token spans that are
illegal in Python – |>, *|>, and the rest – into legal ones. Most of
the pipe family rewrites to the bitwise-or token |. After this pass the
source is valid (though not yet meaningful) Python.
Stage 2 – instrumented execution. The rewritten code is then run in an
instrumented fashion. pipescript remembers where each rewrite happened, so
that the resulting ast.BinOp node can be associated with the original
|> operator. Instrumentation emits an event for that node, and pipescript’s
handler for the event implements the pipe semantics instead of a real bitwise
or.
In short: pipescript rewrites its surface syntax to valid Python, then changes the behavior of specific AST nodes at run time.
The pyccolo backend
The heavy lifting is done by pyccolo, a
library for declarative, event-driven AST instrumentation. You register handlers
for AST node types (such as ast.BinOp); pyccolo instruments those nodes
to emit events, and when the code runs, every registered handler for an event
fires.
The crucial property is composability. Because a single event-emitting
transform can feed many independent handlers, layering several instrumentations
usually Just Works – you do not get the conflicts you would hit hand-writing a
pile of ast.NodeTransformer passes that each rewrite the tree. That is
what lets pipescript’s own features (pipes, placeholders, macros, brace blocks,
optional chaining) be built as separate tracers that coexist, and it is what
lets pipescript coexist with other pyccolo-based tools like ipyflow.
Consequences
Because the instrumented code is still Python and stays visually close to the pipescript source:
Standard and third-party libraries work unchanged – it is all just function calls underneath.
Jupyter ergonomics like readable tracebacks and jedi-based autocomplete keep working, for the most part. Completion is additionally type aware: every pipeline in the cell is desugared into the nested calls it denotes before jedi sees it, so
["a", "b"] |> "\n".join($) |> $.completesstrmethods without the pipeline prefix having been executed – and a name that a pipeline binds (via=or|>>) is typed wherever it is later used. The desugaring models the macros and the compose/partial operators as well, down to the container type thatmapandfilterrestore; where it cannot model a construct soundly it declines to lower it rather than guess at a type.There is a run-time cost when pipescript syntax is actually used, but none for vanilla Python, even with the extension loaded. See Performance.
A Python superset, deliberately scoped
pipescript is effectively a Python superset for interactive use. It is not (for now) a general-purpose functional language and is explicitly not intended for production; it optimizes for simplicity, readability, and predictability over feature completeness. See Installation for the supported environments.