Skip to content

astero.python

Python's grammar, its scope tables, and its namespaces. The one language that ships with the library; everything else in astero is language-agnostic.

astero.python.lang

Python's grammar, declared: CPython's node table plus the roles it omits.

Not a parser, and not a replacement for ast. ast already answers what fields a production has and what each holds: Assign.targets is list[expr] and Assign.value is expr. What it cannot answer is that the first introduces a name and the second does not. To ast the two are both expr, and the difference between them exists only in the language reference and in the head of whoever writes the pass.

Supplying that difference is the whole content of this module. Of Python's 176 fields across 124 productions, 138 are child and have nothing to say; the other 38 carry a role, and 22 positions introduce a variable name — from the obvious Assign.targets to MatchMapping.rest, comprehension.target and withitem.optional_vars. That list is what every Python tool re-derives by hand, and what it misses one of.

The rest is machinery to avoid restating what CPython already records: sorts and shapes come from the running interpreter, roles are authored here. The stdlib cannot be annotated in place, so the role table is the one piece written by hand, and tests/a_unit/test_lang_py.py gates it against CPython's own parser.

Sorts and shapes are free from ast._field_types on 3.13 and later. On 3.11 and 3.12 only field names are exposed, so Field.sort is None there and the identifier sorts are supplied by the same role table that names the positions. That costs nothing here, because every derivation this module feeds keys off roles rather than sorts.

defers_annotations

defers_annotations(tree: AST) -> bool

Whether tree evaluates its annotations lazily, per PEP 649.

False before Python 3.14, and false for a module carrying from __future__ import annotations: PEP 563 makes every annotation a string, so nothing is deferred and CPython compiles no __annotate__ at all.

annotation_blocks

annotation_blocks(tree: AST) -> dict[str, Scope]

ANNOTATION_BLOCKS if they are in force for tree, otherwise none.

Pass the result as scope_tree's siblings. Whether the blocks exist is a property of the whole module rather than of any node, which is why this is a function of the tree and not an entry in a table.

mangle

mangle(name: str, class_name: str) -> str

Python's private name mangling, as the compiler applies it.

An identifier written __x inside class C is compiled as _C__x. A renaming pass that skips this renames a name the interpreter never sees.

build

build(module: ModuleType = ast, name: str = '') -> Grammar

Python's grammar as this interpreter reports it, plus the roles above.

module is ast by default. A compiler that generates its own node classes passes its module instead, and authors nothing: the roles are facts about Python, and a generated class hierarchy holds the same fields under the same names. What differs is which productions exist, so a host missing one gets a grammar missing it too, and building that node fails at the rewrite rather than downstream.