Skip to main content

A Python library for generating Python code via AST construction.

Project description

A Python library for generating Python code via AST construction.

Documentation

Overview

fluent-codegen provides a set of classes that represent simplified Python constructs (functions, assignments, expressions, control flow, etc.) and can generate real Python ast nodes. This lets you build correct Python code programmatically without manipulating raw AST or worrying about string interpolation pitfalls.

Originally extracted from fluent-compiler, where it was used to compile Fluent localization files into Python bytecode.

Key features

  • Safe by construction — builds AST, not strings, eliminating injection bugs

  • Scope management — automatic name deduplication and scope tracking

  • Simplified API — high-level classes (Function, If, Try, StringJoin, etc.) that map to Python constructs without requiring knowledge of the raw ast module, plus two levels of helpers for building up expressions:

  • Security guardrails — blocks calls to sensitive builtins (exec, eval, etc.)

Installation

pip install fluent-codegen

Requires Python 3.12+.

Quick example

This builds a FizzBuzz function entirely via the codegen API, using fluent method-chaining for expressions:

from fluent_codegen import codegen

# 1. Create a module and a function inside it
module = codegen.Module()
func, _ = module.create_function("fizzbuzz", args=["n"])

# 2. A Name reference to the "n" parameter (Function *is* a Scope)
n = func.name("n")

# 3. Build an if / elif / else chain
if_stmt = func.body.create_if()

#    if n % 15 == 0: return "FizzBuzz"   — fluent chaining
branch = if_stmt.create_if_branch(n.mod(codegen.Number(15)).eq(codegen.Number(0)))
branch.create_return(codegen.String("FizzBuzz"))

#    elif n % 3 == 0: return "Fizz"
branch = if_stmt.create_if_branch(n.mod(codegen.Number(3)).eq(codegen.Number(0)))
branch.create_return(codegen.String("Fizz"))

#    elif n % 5 == 0: return "Buzz"
branch = if_stmt.create_if_branch(n.mod(codegen.Number(5)).eq(codegen.Number(0)))
branch.create_return(codegen.String("Buzz"))

#    else: return str(n)
if_stmt.else_block.create_return(module.scope.name("str").call([n]))

# 4. Inspect the generated source
print(module.as_python_source())
# def fizzbuzz(n):
#     if n % 15 == 0:
#         return 'FizzBuzz'
#     elif n % 3 == 0:
#         return 'Fizz'
#     elif n % 5 == 0:
#         return 'Buzz'
#     else:
#         return str(n)

# 5. Compile, execute, and call the generated function
code = compile(module.as_ast(), "<fizzbuzz>", "exec")
ns: dict[str, object] = {}
exec(code, ns)
fizzbuzz = ns["fizzbuzz"]
assert fizzbuzz(15) == "FizzBuzz"
assert fizzbuzz(9)  == "Fizz"
assert fizzbuzz(10) == "Buzz"
assert fizzbuzz(7)  == "7"

Even simpler with E-objects

The example above uses the method-chaining API (n.mod(...).eq(...)), which maps one-to-one to AST nodes. For expression-heavy code, where you know the names of functions/methods/attributes statically, the E-object API lets you use normal Python operators instead — the library intercepts them and builds the AST for you.

Here’s the same FizzBuzz with E-objects:

from fluent_codegen import codegen

module = codegen.Module()
func, _ = module.create_function("fizzbuzz", args=["n"])
n = func.name("n")

if_stmt = func.body.create_if()

# n.e enters "E-object mode" — then % and == are Python operators
branch = if_stmt.create_if_branch(n.e % 15 == 0)
branch.create_return(codegen.String("FizzBuzz"))

branch = if_stmt.create_if_branch(n.e % 3 == 0)
branch.create_return(codegen.String("Fizz"))

branch = if_stmt.create_if_branch(n.e % 5 == 0)
branch.create_return(codegen.String("Buzz"))

# Convenient access to builtins as E-objects via `Scope.enames`
if_stmt.else_block.create_return(module.enames.str(n))

The generated output is identical. The key difference is readability: n.e % 15 == 0 vs n.mod(codegen.Number(15)).eq(codegen.Number(0)).

E-objects really shine for math-heavy expressions:

module = codegen.Module()
_, math_lib = module.create_import("math")
func, _ = module.create_function("distance", args=["x", "y"])
x = func.name("x")
y = func.name("y")

# E-object — reads like the code it generates
func.body.create_return(math_lib.e.sqrt(x.e ** 2 + y.e ** 2))

print(module.as_python_source())
# import math
# def distance(x, y):
#     return math.sqrt(x ** 2 + y ** 2)

Compare with the equivalent method-chaining version:

func.body.create_return(
    math_lib.attr("sqrt").call([
        x.pow(codegen.Number(2)).add(y.pow(codegen.Number(2)))
    ])
)

License

Apache License 2.0

AI/LLM usage

This project has been lovingly created by a human! The original API design came from fluent-compiler and was all written by hand.

Since then, it has been turned into a separate project with considerable help from a coding agent. The agent was used for a variety of tasks, including:

  • extracting the code from fluent-compiler and project “scaffolding” work

  • adding documentation

  • continuing the existing patterns to cover more Python syntax

  • researching which common Python syntax the project did not yet cover

  • improving test coverage

At the same time, the code created was checked carefully and a lot of work was done without the agent to ensure the API worked exactly the way I wanted. Commits created mainly by the agent are obvious in the git history.

For external contributions, I prefer either code written by a human, or a request written by a human, since it is easier for me to prompt the coding agent according to my own standards than to review the results of someone else prompting an AI.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fluent_codegen-0.7.0.tar.gz (53.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fluent_codegen-0.7.0-py3-none-any.whl (30.6 kB view details)

Uploaded Python 3

File details

Details for the file fluent_codegen-0.7.0.tar.gz.

File metadata

  • Download URL: fluent_codegen-0.7.0.tar.gz
  • Upload date:
  • Size: 53.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.2","id":"zara","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fluent_codegen-0.7.0.tar.gz
Algorithm Hash digest
SHA256 0066ac3c423075ea2f2d026109667f8d0f5720098de03b995165e08812fce25f
MD5 a4703680b8abf82ca00b37b6c035ab7e
BLAKE2b-256 11fdb92cb354ed3108df362ed8241b5dc0982ff916970bccaf047822632fe8c8

See more details on using hashes here.

File details

Details for the file fluent_codegen-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: fluent_codegen-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 30.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.2","id":"zara","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fluent_codegen-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 23ca5285e68cd514b62afd607d3dc7222a736e5fe04d66b01aad2586d10dabe3
MD5 24c02589c747109a8d5c134c669fa6b1
BLAKE2b-256 d905c1bf0047fa041b3e841555c44029261608fcfb2043bb7d7eb5dd823187a3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page