PyAST: A Python AST Parser, Written in TypeScript

5 min read

PyAST cover

Python has a built in ast module that turns source code into a tree of nodes you can inspect, transform, and walk. It is the foundation for linters, formatters, and all sorts of code analysis tools in the Python ecosystem. There has never been a good equivalent for people working in a JavaScript or TypeScript environment who need to understand Python code without shelling out to a Python process. That is the gap PyAST fills.

What is PyAST

PyAST is a TypeScript library that parses Python source code and produces an Abstract Syntax Tree that mirrors the shape of CPython’s own ast module, following the same ASDL grammar specification Python itself is built on. Node types, field names, and position metadata like lineno, col_offset, end_lineno, and end_col_offset all match what you would get from ast.parse() in Python.

It is not just a one way parser either. PyAST can also convert an AST back into Python source code with unparse(), which makes round trip parsing, transforming, and regenerating code possible entirely in JavaScript.

You can try it directly in the browser at pyast.nepcodex.com, and the source is on GitHub at github.com/kriss-u/py-ast.

Why build a Python parser in TypeScript

Most tools that need to understand Python source code end up spawning a Python subprocess, which means dragging a Python runtime into a Node.js or browser environment, managing IPC, and dealing with startup overhead. For tools that live natively in JavaScript, editors, static analysis tools, educational playgrounds, code transformation pipelines, that dependency is awkward at best.

PyAST removes that dependency entirely. It is a pure TypeScript implementation with no native bindings and no subprocess, so it runs anywhere JavaScript runs, including directly in the browser.

What it can do

At its core, PyAST covers the same ground as Python’s ast module.

  • Lexical analysis - a full tokenizer for Python source, including f-strings, indentation sensitive blocks, and the usual set of operators and literals.
  • AST generation - parse() builds a complete tree for any valid Python source, whether it is a bare expression, a single statement, or a full module.
  • Code generation - unparse() converts an AST back into readable Python source.
  • Traversal - walk() iterates over every node in a tree, and NodeVisitor and NodeTransformer give you a structured way to visit or rewrite specific node types.
  • JSON serialization - dump an AST to JSON for storage or for feeding into other tools.
  • Safe literal evaluation - literalEval() evaluates Python literals (numbers, strings, lists, dicts, tuples, sets, booleans, None) without executing arbitrary code, similar to ast.literal_eval().

Parsing looks about the way you would expect if you have used Python’s own ast module:

import { parse, unparse } from "py-ast";

const pythonCode = `
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

result = fibonacci(10)
`;

const ast = parse(pythonCode);
console.log(ast.nodeType); // "Module"

const regeneratedCode = unparse(ast);
console.log(regeneratedCode);

Traversal follows the same visitor pattern Python developers already know from ast.NodeVisitor:

import { NodeVisitor, parse } from "py-ast";

class CodeAnalyzer extends NodeVisitor {
  functions: string[] = [];
  classes: string[] = [];

  visitFunctionDef(node: any) {
    this.functions.push(node.name);
    this.genericVisit(node);
  }

  visitClassDef(node: any) {
    this.classes.push(node.name);
    this.genericVisit(node);
  }
}

const analyzer = new CodeAnalyzer();
analyzer.visit(parse(pythonCode));

console.log(analyzer.functions);
console.log(analyzer.classes);

And NodeTransformer lets you rewrite parts of the tree and get valid Python back out through unparse(), which is the pattern you would reach for to build a small code refactoring tool or a linter with autofix support.

PyAST also handles the parts of Python that tend to trip up naive parsers, comprehensions, async/await, decorators, f-strings, and optional comment parsing that attaches hash comments to the AST with their line numbers.

Try it in the playground

Reading about a parser is one thing, seeing it work is another. The playground runs PyAST directly in the browser. Paste in Python code and you get the parsed AST, the JSON representation, and the unparsed output, all live, with no backend involved.

It is a fast way to check how a particular piece of syntax gets represented in the tree, or to confirm that a round trip through parse() and unparse() preserves the behavior you expect.

Installing it

PyAST is published on npm and JSR as py-ast:

npm install py-ast
import {
  literalEval,
  NodeTransformer,
  NodeVisitor,
  parse,
  unparse,
  walk,
} from "py-ast";

Full API documentation is available at kriss-u.github.io/py-ast.

Where this is useful

The direct motivation was tooling: linters, static analyzers, and educational tools that want to reason about Python code without shipping a Python interpreter alongside them. A browser based Python teaching tool can show a live AST as a student types. A JavaScript based build pipeline can statically check Python scripts without invoking a subprocess. A code migration tool can parse, transform, and regenerate Python source entirely within a Node.js process.

Try it yourself

Head over to the playground and paste in some Python code to see the AST come out live. If you want to use it in a project, install it from npm, or dig into the source and contribute on GitHub.

Feedback and contributions are always welcome.

Related Posts