Skip to main content

astichi

PyPI version Python versions License

A declarative, runtime code generator for Python.

Astichi stitches small, marker-bearing Python snippets into specialized code at runtime — for example, inside a class decorator that assembles a tailored implementation each time it is applied — and emits plain, inspectable Python that then runs with no per-call dispatch overhead. An optional native Rust engine keeps that runtime generation fast enough to sit on the decoration / import path.

The point is to make AST stitching declarative: you say what fills which hole and what satisfies which injection point — even partially — and astichi connects the dots for you. It matches snippets to compatible holes, values to their binding slots, and identifiers to their demands (with a real compatibility check, not string-matching), wires them across nested layers, and keeps every scope hygienic. You describe the graph; you don't hand-walk and mutate an AST.

It is a focused, hygienic AST stitcher — it composes generator-authored fragments; it is not a generic codemod or refactoring framework for existing source.

pip install astichi

Release wheels for Linux, macOS, and Windows (CPython 3.12–3.15) bundle an optional native Rust acceleration engine; installs without a matching wheel fall back to pure Python. See the native fast path.

Quick start

You author Python with markers in it, compile each snippet into a Composable, wire them together with a builder, then materialize and emit real Python.

import astichi

root = astichi.compile("""
items = []
astichi_hole(body)            # a named insertion site
result = tuple(items)
""")

step = astichi.compile("""
astichi_pass(items, outer_bind=True).append("x")   # explicitly reuse outer `items`
""")

builder = astichi.build()
builder.add.Root(root)
builder.add.Step(step)
builder.Root.body.add.Step(order=0)   # stitch `step` into the `body` hole

print(builder.build().materialize().emit(provenance=False))

Emitted Python:

items = []
items.append("x")
result = tuple(items)

Without astichi_pass(items, outer_bind=True), the inner snippet would not silently reuse items just because the spelling matches. Astichi defaults to isolated scopes and only crosses them when the source says so — that is the hygiene guarantee that makes large stitched programs predictable.

For the full compile → bind → build → describe → materialize → emit walkthrough, see the Using the API guide.

What you get

  • Connect-the-dots wiring. Hand astichi a snippet, a value, or an identifier plus a partial description of where it goes, and it finds the compatible hole / binding slot / demand and attaches it — checking structural compatibility, not just names. You under-specify; it resolves the match (and refuses ambiguous ones with a diagnostic). This is the declarative core; see the assembler.
  • Multi-layer composition. Composables compose into composables: build one, reuse it as a piece of the next, nest child scopes, and wire identifiers across layers — with hygiene preserved at every boundary.
  • Valid ASTs, not string fragments. Compose typed AST nodes with deterministic insertion order instead of concatenating text.
  • Hygiene by default. Each inserted snippet lives in its own scope. Names cross boundaries only through explicit keep / pass / import / export, so stitched fragments never collide by accident.
  • Generation-time specialization. Bake values into the source and unroll astichi_for(...) loops into straight-line Python as you generate — so the emitted function carries no dispatch layer to pay for on every call.
  • Managed imports. Snippets declare the imports they need with astichi_pyimport(...); astichi collects, dedupes, collision-checks, and inserts them at materialize time.
  • Inspectable output. Emitted source can be diffed, tested, and round-tripped, optionally with a provenance tail for AST/source-location restoration.
  • Descriptor-driven composition. describe() exposes holes, binds, ports, and target addresses so tools can wire fragments from data instead of hand-written attribute chains.

The marker model

These are not functions you import or call. astichi_hole, astichi_keep, astichi_pass, and the rest are markers — sentinel names recognized in the Python source text you hand to astichi.compile(...). There is no from astichi import astichi_hole; you write the marker inside the snippet string and astichi recognizes it by name and AST position (not by string matching alone). The only names you actually import from astichi are compile, build, Composable, and a few helpers.

The core markers are:

  • astichi_hole(name) -> insertion site
  • astichi_keep(name) -> hygiene-preserved name in expression / statement source
  • name__astichi_keep__ -> hygiene-preserved name in identifier position
  • name__astichi_arg__ -> identifier demand
  • name__astichi_param_hole__ -> function-parameter insertion target
  • astichi_funcargs(...) -> call-argument payload
  • astichi_for(...) -> build-time loop unrolling
  • astichi_bind_external(name) -> external/literal value slot
  • astichi_ref(path) -> compile-time reducible identifier / attribute path
  • astichi_pyimport(module=..., names=(...)) -> managed Python import
  • astichi_comment("...") -> final-output source comment
  • astichi_pass(name, outer_bind=True) -> explicit same-name boundary read
  • astichi_import(name) -> explicit whole-scope boundary import
  • astichi_export(name) -> explicit outward supply
  • astichi_insert(...) -> internal emitted metadata, not general authored API

Comment marker note:

  • astichi_comment("...") is statement-only. Ordinary materialize() strips it for executable output; emit_commented() renders it as real # comments.
  • Multi-line payloads keep the marker statement's indentation, and only exact {__file__} / {__line__} substrings are expanded.

Value-form target note:

  • astichi_ref(...) and astichi_pass(...) are ordinary value-form surfaces in expressions.
  • If the marker result itself must occupy an Assign / AugAssign / Delete target position, append ._ or .astichi_v: astichi_ref("self.f0")._ = 1, astichi_pass(counter).astichi_v = 1.
  • If you immediately continue to a real attribute, plain Python target syntax already works: astichi_pass(obj).field = 1.

The one rule that matters most is scope:

  • astichi_insert is the basic Astichi boundary.
  • Each inserted composable lives in its own Astichi scope.
  • There is no implicit capture across that boundary.
  • If a name crosses the boundary, make it explicit with keep, pass, import, or export.
  • Function parameters are the pinned exception: parameter names and uses in the function scope stay attached to that parameter binding.

The full marker reference, edge cases, and value-form target rules live in the marker docs and scoping & hygiene reference.

Declarative wiring: the assembler connects the dots

This is the part that makes astichi feel low-fuss. The hardest, most error-prone part of stitching code by hand is the bookkeeping: which snippet is allowed in which hole, which value feeds which injection point, which identifier answers which demand — and keeping all of that straight as the graph grows across layers. astichi.assembler.AssemblyScope does that matching for you.

The plain fluent builder makes you name the exact target up front (builder.Root.body.add.Step()). The assembler inverts it: you hand it a resource plus a partial description of where it belongs, and it finds the site that resource can legally satisfy — a block hole, an identifier demand, an external-value slot — using a structural compatibility check against the target's descriptor, not string-matching. Under-specify and it resolves the match; if more than one site fits, it refuses with a diagnostic rather than guessing. That is the "connect the dots" behavior: you declare intent, astichi does the wiring.

It is built from three small, composable pieces:

  1. Pluggable resources — what to attach: as_composable(...) (a fragment), as_external_value(...) (a compile-time value), as_identifier(...) (an identifier spelling).
  2. Partial selectors — where it may attach, as much or as little as you know: name, build_match, and owner_match (the last two are path patterns with . / ? / * / + wildcards).
  3. One-call resolution — wire(resource, ...) runs find_candidates(...) → require_one(...) → apply(...) for you and returns the resolved candidate. (The three steps stay public if you want them separately, and apply_batch(...) takes an ordered stream.) wire raises a diagnostic naming the build path, owner, and source location when the selector matches nothing or more than one site.
import astichi
from astichi.assembler import AssemblyScope, as_composable, as_external_value

root = astichi.compile("""
out = []
astichi_hole(body)
result = tuple(out)
""")
body = astichi.compile("""
astichi_pass(out, outer_bind=True).append(astichi_bind_external(label))
""")

scope = AssemblyScope(astichi.build())
scope.add("Root", root)

# One call each, minimal selector — just the demand NAME. No exact target path.
scope.wire(as_composable(body, build_name="Body"), name="body")
scope.wire(as_external_value("done"), name="label")

print(scope.build().materialize().emit(provenance=False))

Emitted Python:

out = []
out.append('done')
result = tuple(out)

You named the demand (body, label) and nothing else — no builder.Root.body.add(...), no exact address. The scope found the compatible hole and the compatible external slot and wired them.

When you do need precision — disambiguating among many sites, indexed instances, ordering — tighten the same call with build_match / owner_match path patterns (. / ? / * / + wildcards), build_index, and order. That is also what lets you compose along two axes at once: place the same template in many spots (structure), and specialize each placement differently (substitution). One template becomes many polymorphic concrete forms:

import astichi
from astichi.assembler import (
    AssemblyScope, as_composable, as_external_value, as_identifier,
)

shell = astichi.compile("""
class Accessors:
    def __init__(self, data):
        self._data = data
    astichi_hole(methods)
""")

# One polymorphic template: the method name is an identifier demand,
# the lookup key is an external-value slot.
getter = astichi.compile("""
def method_name__astichi_arg__(self):
    return self._data[astichi_bind_external(key)]
""")

scope = AssemblyScope(astichi.build())
scope.add("Shell", shell)

for i, (method, key) in enumerate(
    [("get_name", "name"), ("get_email", "email"), ("get_age", "age")], start=1
):
    inst = f"Getter[{i}]"
    # axis 1 — structure: place the SAME template into `methods`, repeatedly
    scope.wire(as_composable(getter, build_name="Getter", build_index=i, order=i), name="methods")
    # axis 2 — specialization: bind THIS instance's name + key differently
    scope.wire(as_identifier(method), name="method_name", build_match=("Shell", inst))
    scope.wire(as_external_value(key), name="key", build_match=("Shell", inst))

print(scope.build().materialize().emit(provenance=False))

Emitted Python — three specialized methods from one template:

class Accessors:

    def __init__(self, data):
        self._data = data

    def get_name(self):
        return self._data['name']

    def get_email(self):
        return self._data['email']

    def get_age(self):
        return self._data['age']

That is the polymorphic core: a single template, matched into a hole three times and specialized per instance — and as_composable, as_identifier, and as_external_value resources all attached by the same one-call wire(...). YIDL pushes this to production scale (see Maturity).

Least-surprise means it never guesses. If a resource fits more than one site, wire refuses (via require_one) with a diagnostic that names every candidate's build path, owner, demand name, kind, and source location, so you know exactly how to narrow the selector:

ValueError: expected exactly one candidate, found 2
candidate 1:
  demand: build_path=Root owner=. name=first kind=hole.block location=<astichi>:3 locator=body[1]/value
  resource: composable build_name=Frag
    production: name=__block__ kind=production.block location=<astichi>:1 locator=.
candidate 2:
  demand: build_path=Root owner=. name=second kind=hole.block location=<astichi>:4 locator=body[2]/value
  resource: composable build_name=Frag
    production: name=__block__ kind=production.block location=<astichi>:1 locator=.

The same matching works across layers. A composable you already built can be registered as a piece of a larger assembly, child scopes resolve before their parents, and an identifier supplied in one layer can answer a demand in another — so you compose composables, and astichi keeps the wiring and the hygiene consistent the whole way up the tree.

Composables carrying astichi_pyimport(...) markers also have their imports auto-linked and deduped at materialize, and the materialization plan tracks both boundary hygiene and managed-import hygiene so independently authored fragments never silently collide. Full reference: Assembler Scope.

Native Rust fast path

The target workload is runtime code generation — e.g. a class decorator that assembles a tailored implementation every time it is applied, across many classes at import time. Pure-Python AST assembly is too slow to sit on that per-use path, so astichi ships an optional native engine (_astichi_native_engine, built with PyO3) that drives the hot generation path in Rust.

When it is present, lower-engine selection defaults to auto and prefers native Rust for the assembler's batch resolve/apply, keeping occurrence/edge state in the native engine. When the extension is absent, the exact same API runs on pure Python — native is an accelerator, never a requirement.

  • Release wheels bundle the extension; pip install from sdist compiles it when no matching wheel exists. Set ASTICHI_SKIP_NATIVE_BUILD=1 for a Python-only install.
  • Build locally from the repo root with uv run python native_engine/build.py.
  • Native source builds require Rust 1.85 or newer; prebuilt wheels need no Rust installation.
  • engine=python remains the differential oracle that the native path is tested against.

See native_engine/README.md and the perf-refactor notes in dev-docs/ for the self-native production boundary.

Maturity

Astichi is a 1.x release backed by a suite of 2,250+ tests — golden source/plan fixtures, structural snapshots, and integration coverage — run across CPython 3.12–3.15 and against both the Python and native lower engines, with engine=python serving as a differential oracle for the Rust path. Behavior is anchored by those golden and differential tests; the core compile → build → materialize → emit pipeline is stable, and new surface lands behind the same test discipline.

In the wild. Astichi is the code-generation engine behind YIDL lifecycle: ~6,400 lines of declarative .yidl across 8 layered concepts compile to 118 reusable templates and 310 match/contribution rules, which the assembler weaves into ~4,800 lines of generated lifecycle code — all on both axes (one template, many specialized placements) plus a concept-inheritance layer on top, driven through AssemblyScope, not by hand. That is the polymorphic, declarative model above at production scale.

Documentation

Topic Doc
Docs home docs/README.md
End-to-end guide (compile → emit) guide/using-the-api.md
Reference index reference/README.md
Glossary reference/glossary.md
Public API & submodules reference/public-api.md
compile(...) reference/compile-api.md
Composable, emit, materialize reference/composable-api.md
Builder (fluent + data-driven) reference/builder-api.md
Descriptors (describe()) reference/descriptor-api.md
Markers reference/marker-overview.md
Scoping & hygiene reference/scoping-hygiene.md
Managed imports reference/marker-pyimport.md
Materialize & emit reference/materialize-and-emit.md
Assembler scope (auto-attach) reference/assembler-scope.md
Implementation snapshot & open gaps dev-docs/AstichiSingleSourceSummary.md

When astichi is not the right tool

  • You want to refactor or rewrite an existing user codebase — astichi composes generator-authored fragments, it is not a generic ast.NodeTransformer-style codemod or refactoring framework.
  • You need a one-off source transform — astichi earns its keep when the same generator runs repeatedly (e.g. a decorator applied across many classes) and ordering, scope, hygiene, and speed all matter at once.
  • A plain string template is genuinely enough and none of those concerns are fighting you — you may not need astichi.

Development

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest

Or with uv:

uv run --with pytest pytest -q

Build the native extension (optional): uv run python native_engine/build.py.

License

LGPL-2.1-or-later. See LICENSE.

Metadata

Release files for astichi 1.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for astichi 1.1.3
File Size Uploaded
astichi-1.1.3.tar.gz 974.1 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for astichi 1.1.3
File
astichi-1.1.3-cp314-cp314-win_amd64.whl CPython 3.14 CPython 3.14 Windows x86-64 Details
astichi-1.1.3-cp313-cp313-win_amd64.whl CPython 3.13 CPython 3.13 Windows x86-64 Details
astichi-1.1.3-cp312-cp312-win_amd64.whl CPython 3.12 CPython 3.12 Windows x86-64 Details
astichi-1.1.3-cp312-abi3-manylinux_2_28_x86_64.whl CPython 3.12 abi3 Linux glibc 2.28+ x86-64 Details
astichi-1.1.3-cp312-abi3-manylinux_2_28_aarch64.whl CPython 3.12 abi3 Linux glibc 2.28+ ARM64 Details
astichi-1.1.3-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details
astichi-1.1.3-cp312-abi3-macosx_10_13_x86_64.whl CPython 3.12 abi3 macOS 10.13+ x86-64 Details

Total release size: 19.7 MB

Release files / astichi-1.1.3.tar.gz

Download URL astichi-1.1.3.tar.gz
Size 974.1 kB
Tags Source
SHA-256 checksum
How to use checksums
120cbb15ce6f186d7aa3f47562a90a59291e79657fbe5f3ee40d5a053836b4dd
BLAKE2b-256 checksum
How to use checksums
c091e150c098aada716b02b3fd6b090b0cf4ac06ee6e65c867056941a16ab034
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / astichi-1.1.3-cp314-cp314-win_amd64.whl

Download URL astichi-1.1.3-cp314-cp314-win_amd64.whl
Size 5.7 MB
Tags CPython 3.14 Windows x86-64
SHA-256 checksum
How to use checksums
8d511c49ecd83ff321ac65ab068f28aaef0071488d0331b5b36a3d25b907adee
BLAKE2b-256 checksum
How to use checksums
ad65c159cae6444ed8b741dd1f36265932e1f09658eadef80aa1e74a2a1241e4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / astichi-1.1.3-cp313-cp313-win_amd64.whl

Download URL astichi-1.1.3-cp313-cp313-win_amd64.whl
Size 3.6 MB
Tags CPython 3.13 Windows x86-64
SHA-256 checksum
How to use checksums
d5b871519db7027af96524bb323890505c9fafe1618c299596e0e164b6f66a7c
BLAKE2b-256 checksum
How to use checksums
173542b7422e9ba8ace816c7af15dca469aa9e709bbfaaeab60d6b1e3a5a988d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / astichi-1.1.3-cp312-cp312-win_amd64.whl

Download URL astichi-1.1.3-cp312-cp312-win_amd64.whl
Size 2.0 MB
Tags CPython 3.12 Windows x86-64
SHA-256 checksum
How to use checksums
7235736bff24ca8de5e6fcc576ad72a65d90c3a9254d1b191a1804c8b71f297b
BLAKE2b-256 checksum
How to use checksums
4b4db6832e452f5c1f8c27f7e23090e1512fd2c0552592d4cd0819f2c03e5273
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / astichi-1.1.3-cp312-abi3-manylinux_2_28_x86_64.whl

Download URL astichi-1.1.3-cp312-abi3-manylinux_2_28_x86_64.whl
Size 1.9 MB
Tags CPython 3.12 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
544da9336a8bbcf11fd693db25ebc639a8d6ae15b2f887439d11a7d55f2f4a14
BLAKE2b-256 checksum
How to use checksums
96eba4941feb3eb2a26ff5b1b166310d6461b18bb556cba2322f50c91311614c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / astichi-1.1.3-cp312-abi3-manylinux_2_28_aarch64.whl

Download URL astichi-1.1.3-cp312-abi3-manylinux_2_28_aarch64.whl
Size 1.8 MB
Tags CPython 3.12 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
72c49a26fd89e72c1129ec3b62da4b1d4aa1b9d02cdb3e4a785c548997c174ef
BLAKE2b-256 checksum
How to use checksums
b9f34baee3d22c2cfdb0c87913c1f7ba8f185f4a2630c98a0607dcb0cd8c2cfe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / astichi-1.1.3-cp312-abi3-macosx_11_0_arm64.whl

Download URL astichi-1.1.3-cp312-abi3-macosx_11_0_arm64.whl
Size 1.8 MB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
d817e3588a79a6a81bd16fe94a53da6f3095f6b48e67a059f96b2d3cac7985c9
BLAKE2b-256 checksum
How to use checksums
82b09626567b917bbcd0e381c08168be6d9a417f83c45d727895ee641811e2ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / astichi-1.1.3-cp312-abi3-macosx_10_13_x86_64.whl

Download URL astichi-1.1.3-cp312-abi3-macosx_10_13_x86_64.whl
Size 1.9 MB
Tags CPython 3.12 abi3 macOS 10.13+ x86-64
SHA-256 checksum
How to use checksums
422204b0df0f0bb54b80e95386f469e6f21a2778838f22bae4428dac090f7d12
BLAKE2b-256 checksum
How to use checksums
09859fb8cb84f80cf20e59475904fc27711b882312c18d78b0a9ae36f5ca64d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.3 This release

8 release files

1.1.2

8 release files

1.1.1

8 release files

1.1.0

8 release files

1.0.7

8 release files

1.0.5

8 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page