Skip to main content

PyPrior

A constructive prior over Python programs: sample them, condition on prompts, or use the prior as a constrained decoder for model-generated code.

A function with a for-loop and an if/else assembling itself one legal token at a time, then running

A single free-form sample, sampled from a random transformer network under PyPrior's constraints — a function with a for loop and an if/else (each branch returns), a comparison used as a value, and nested calls, assembled one legal token at a time and then run. Scope-safe, no dead code. Reproduce it (and the GIF) with python examples/api/demo_gif.py.

PyPrior samples programs from a restricted Python grammar by deciding, at every step, the exact set of tokens that are legal next — so every program it produces is syntactically valid, scope-correct (no undefined variables), and bounded in size by construction. Its token stream is a preorder serialization of the restricted Python AST.

The GIF above shows a nontrivial program with loops and branches. That program uses 47 PyPrior tokens; the same source costs 111 GPT-2 tokens. PyPrior's AST vocabulary is compact, and AST-token generation spends tokens on program structure instead of source-code spelling.

What existing constrained decoders miss

Existing constrained decoding libraries such as SynCode, XGrammar, and Outlines are valuable when a normal text-token LLM has already learned a Python prior from lots of human-written code and you want to keep its output inside a grammar or structured format. If the sampler or model has no Python prior, a syntax mask alone is not enough. PyPrior targets that different problem: a Python AST-token language with scope tracking, bounded completion, direct execution semantics, and useful unconditional sampling.

For example, uniform random sampling through SynCode's Python grammar mask with a GPT-2 tokenizer can produce syntactically valid Python, but GPT-2's token distribution is not a useful Python program prior. A prompt-conditioned sample can look like:

def f(x):
     KCatioushaIntroductioneeAddedRushymmWrittennyderescalALTHpecAccess713EyeensitiveenderedtriggerRahredditmenuappedicheixtpossiblybandJJwatersORD

That parses as Python, but it is just a bare expression using an undefined name. The constrained decoder did its job: it controlled syntax. It did not provide a program prior, scope safety, or execution semantics.

PyPrior samples directly in an AST vocabulary, so even uniform random sampling produces structured, scope-safe, runnable programs — every name is bound, there is no unreachable code, and every program is guaranteed to terminate (no while, no recursion — only bounded for/range loops and calls to already-defined functions, so every run halts):

def func():
    for a in range(9, 3 + 5):
        print(a)
    return 2 + 4

print(func())   # runs -> 6
Approach Good at Missing for PyPrior's use case
Text-token grammar masks Keeping an LLM's text output syntactically valid Scope tracking, AST-token output, small controlled vocabulary, bounded AST size/depth, direct execution model
PyPrior Random code augmentation and PyPrior-vocab constrained decoding Full Python coverage, arbitrary libraries/imports, unconstrained human-style code

This is the core distinction: text-level constrained decoders constrain a language model; PyPrior defines the language, the vocabulary, the decoder state, and the executable subset together.

Install

Install directly from the GitHub repo for now:

pip install "pyprior @ git+https://github.com/PatrickHua/pyprior.git"

The core is numpy-only. Optional extras: [demo] (torch, for the sample decoders), [vllm] (the vLLM V1 logits-processor integration), [dev] (pytest, hypothesis, pytest-benchmark).

Using PyPrior

The engine works the same at every step: ask what's legal, advance with any of those tokens, repeat. A random sampler, a weighted prior, and a neural model all plug into that one loop. There are two surfaces.

1. The engine

Drive the decode directly — pick any legal token however you like.

This is the primitive state-machine API that Prior wraps; examples/api/prior.py shows the higher-level model/prior loop built on top of it.

import random
import pyprior as pp

tok = pp.Tokenizer(value_range=(0, 9))
s = pp.init(vocab=tok, max_len=30)                  # a fresh free-form decode
                                                    # (pass ids=[...] to start from a prompt)
while not pp.done(s):
    legal = s.rows[0].legal()                       # the set of legal next token ids
    pp.advance(s, [random.choice(sorted(legal))])   # pick any one — here, uniformly
print(tok.decode(s.ids[0]))

legal (or pp.next_legal_tokens(s) for the additive [B, V] mask a model adds to its logits) is intersected from four sources: the grammar's FIRST set, the in-scope variables (definite assignment), the depth/length guards, and a min-cost-to-complete check that guarantees the program closes within max_len. It is never empty until the program is done.

2. The prior + a model

Prior blends a model's logits, a symbolic prior, and the legal mask — built once, then a session loop. The same prior.apply is what the vLLM processor calls, so behavior is identical whether you own the loop or vLLM does:

import pyprior as pp
from pyprior import PriorConfig
from pyprior.demo import RandomDecoder          # flat-logit stand-in model (the torch extra)

def sample(logits, temperature=1.0):            # [B, V] -> [B]
    import torch
    return torch.multinomial(torch.softmax(logits / temperature, -1), 1)[:, 0]

tok = pp.Tokenizer(value_range=(0, 9))
prior = pp.Prior(tok, PriorConfig(apply="entropy"), max_len=30)  # entropy | fixed | additive | off
model = RandomDecoder(tok.vocab_size)           # flat logits -> entropy ~max -> follows the prior

def function_prefix(arity):
    return (tok.id_of("BLK_1"), tok.id_of("FUNC_1"), tok.id_of(f"ARITY_{arity}"))

prior.start([function_prefix(1), function_prefix(2)])  # or start(n=B) for free-form
while not prior.done():
    logits = prior.apply(model(prior.input_ids)[:, -1])   # combine per PriorConfig.apply
    prior.advance(sample(logits))
prior.show(0)                                   # animate the first sample

Apply modes: entropy (param-free confidence gate — lean on the prior when the model is unsure, fade as it sharpens; default), fixed (convex blend, weight alpha), additive (prior as a bias), off (mask only).

Runnable walk-throughs in examples/api/: prior.py, torch_nn.py (a real torch.nn model), and vllm_adapter.py (serving). For batched production serving the engine ships a vLLM V1 logits processor (pyprior.integrations.vllm.PyPriorLogitsProcessor); the model-free walk-through is tests/integration/test_vllm_processor.py.

What it generates

A small, bounded, free-form subset of Python over integers — a module of top-level statements, where a def is one statement form:

x = 3                     # top-level statements: assign / if / for / def
def func(a, b):           # a def is one of them (params + locals are scoped)
    d = a * b + 1         # assignments to fresh / existing locals
    if d < 0:             # if / else
        return 0          # return is a statement (early returns OK), function-body only
    return d              # ...or fall through to None

The shape is configured entirely on the Tokenizer: value_range (integer range), max_block_stmts, max_expr_depth, max_control_depth, max_funcs, and max_vars. The grammar is otherwise maximal — bounded for loops (one- and two-arg range), non-recursive def calls, print(expr), and comparisons used as int values (b = a == 7 → int(a == 7)) are always available, and the language stays total (recursion is structurally impossible, loops are bounded). By default a return ends its block (no unreachable code) — pass no_dead_code=False to allow statements after a return. To restrict generation, subtract with exclude= rather than a flag: exclude={"CALL"} (no calls), exclude={"for"} (no loops), exclude={"PRINT"} (pure functions). To generate a single function, start from an explicit prefix such as BLK_1 FUNC_1 ARITY_2 and set max_block_stmts=1. init(..., exclude=...) forbids whole forms during generation (e.g. exclude={"binop"} or {"func"}).

Watch it decode

pp.show replays a token stream into the animation above — one legal token at a time, holes (<expr>, <stmt>, <block>) filling in:

import pyprior as pp

tok = pp.Tokenizer(value_range=(0, 9))
ids = pp.init(vocab=tok, max_len=30)               # ...sample a stream (see "Using PyPrior")
pp.show(tok, ids.ids[0])                            # animate in the terminal
pp.show(tok, ids.ids[0], save_as_gif=True, gif_path="decode.gif")   # ...or export a GIF

Status

Supports Integer types. List, strings, ... etc remains TBD.

License

MIT — see LICENSE.

Metadata

Release files for pyprior 0.0.2

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

Source distribution (sdist)

Source distribution for pyprior 0.0.2
File Size Uploaded Provenance
pyprior-0.0.2.tar.gz 49.8 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pyprior 0.0.2
File Interpreter ABI Platform
pyprior-0.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 106.9 kB

Release files / pyprior-0.0.2.tar.gz

Download URL pyprior-0.0.2.tar.gz
Size 49.8 kB
Tags Source
SHA-256 checksum
How to use checksums
e72e4e56f0fdbae7f37b78816e9ee7fd9184bf443abc5f7e2a5bbde206a21221
BLAKE2b-256 checksum
How to use checksums
912cd09a6cec9e12f802223e5910565686230d84f611ed827a42af6e520586b3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.10.13

Release files / pyprior-0.0.2-py3-none-any.whl

Download URL pyprior-0.0.2-py3-none-any.whl
Size 57.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7c7f34820a31de7b76629ccc90aa60b9ebd4fec0f5510fb3af52b7130a9537f3
BLAKE2b-256 checksum
How to use checksums
6d710106fac546ad3cc2c431e9edc7ea2bc961da13542b314d654b2da63d4bbb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.10.13

Security for pyprior 0.0.2

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.0.2 This release

2 release files No provenance

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