Skip to main content

stanli

The Stan Language Interpreter. Compile and sample Stan models with no C++ toolchain on the machine.

PyPI Python License wheels

pip install stanli

That is the whole install. No compiler, no make, no CmdStan checkout, no multi-minute first-run build. One wheel, one shared library, under eight megabytes. Models run without a per-model C++ build; see the current benchmark for measured gradients and setup-plus-2,000-gradient time estimates.

import stanli

model = stanli.Model(stan_file="eight_schools.stan", data="data.json")
fit = model.sample(seed=1, chains=4, warmup=1000, samples=1000)

fit["mu"].mean()        # every draw of a column, chains concatenated
fit.draws("mu")         # (chains, draws), for a trace plot

Stan #include directives automatically search the directory containing stan_file. Add shared directories with include_paths:

model = stanli.Model(stan_file="models/model.stan", data="data.json",
                     include_paths=["shared", "vendor/stan"])

Explicit directories are searched in order, then the input file's directory. For stan_code, the final search directory is the current directory. Nested includes use the same search path; for example, #include nested/helper.stan looks beneath each search directory. Function, stan_to_mir, and bridgestan_model also accept include_paths. Constructed models retain the compiled includes, so later sampling does not reread files that may have changed.

Model(..., fast_math=True) and stan_to_mir(..., fast_math=True) opt in to fast mode, which compiles with fused multiply-adds, lets the runtime regroup sums, and evaluates likelihood terms that repeat over the data once per distinct case. Results can differ from the default in the last bits. It is off by default and needs the bundled portable compiler (stanli-compile) or an embedded build. See fast mode.

Sampling reports CmdStan-shaped progress every 100 transitions by default, followed by per-chain warm-up, sampling, and total times:

Chain [1] Iteration:    1 / 2000 [  0%]  (Warmup)
Chain [1] Iteration: 1000 / 2000 [ 50%]  (Warmup)
Chain [1] Iteration: 1001 / 2000 [ 50%]  (Sampling)
Chain [1] Iteration: 2000 / 2000 [100%]  (Sampling)

Chain [1] Elapsed Time: 0.821 seconds (Warm-up)
                        0.169 seconds (Sampling)
                        0.990 seconds (Total)

Set refresh=0 for a completely quiet run, or another positive integer to change the update interval. Progress is written through Python's sys.stdout, so notebook output and contextlib.redirect_stdout() work normally. Reporting only observes completed transitions: changing refresh does not change draws, sampler statistics, generated-quantity RNG streams, or reproducibility. If any post-warmup transition diverges or saturates the maximum treedepth, the final output also reports the aggregate count. Those counts cover all transitions, including transitions omitted by thinning.

For models using reduce_sum or reduce_sum_static, opt into native within-chain parallelism with model.sample(threads_per_chain=4, parallel_chains=2). This uses up to eight active sampling threads. The default is one thread per chain; small or unsupported reductions stay serial. Inspect model.reduce_sum_count and model.reduce_sum_fallbacks after sampling for retained reductions and graph-lowering refusals. Changing the thread setting rebuilds the prepared model and can change reduction rounding and NUTS draws. The runtime must have thread support. See the native reduction guide for eligibility and memory behavior.

Chains and convergence

Four chains by default, run in parallel, because R-hat needs more than one and a single-chain run cannot be checked for convergence at all. Eight schools does all four in about 70 ms. Scheduling chains changes nothing about the answer: each chain owns its executor and its RNG stream, so the draws come out byte-identical to a sequential run.

print(fit.summary())
name                Mean       MCSE     StdDev         5%        50%        95%   ESS_bulk   ESS_tail      R_hat
mu                4.4600     0.0532     3.1705    -0.7414     4.5519     9.5384       3586       2847      1.000
tau               3.4752     0.0635     3.1612     0.2192     2.6680     9.6313       2160       1874      1.001

R-hat is rank-normalized split-R-hat and ESS is the bulk/tail pair (Vehtari et al. 2021), computed by stan's own estimators, so the numbers agree with stansummary rather than approximating it.

print(fit.diagnose())
No divergent transitions.
No transitions saturated the maximum treedepth of 10.
E-BFMI is above 0.3 in every chain.
R-hat is below 1.01 for every parameter (worst 1.002, theta.6).
Bulk ESS is at least 100 per chain for every parameter (worst 2160, tau).
Tail ESS is at least 100 per chain for every parameter (worst 1874, tau).
No problems detected.

Those are the checks a Bayesian workflow actually turns on, including E-BFMI, the one that catches a badly explored heavy tail, which R-hat and ESS are both blind to. The pieces are reachable individually too: fit.divergences, fit.max_treedepth_hits, fit.stepsize and fit.ebfmi() are per-chain arrays, and fit.to_arviz() hands off an InferenceData with the sampler stats attached.

The mode, and where to start

r = model.optimize(seed=1)
r["mu"], r.lp          # every CSV column at the mode, and the lp there
r.unconstrained        # the point on the sampler's scale

fit = model.sample(inits=r.unconstrained)   # start the chains there

L-BFGS, stan's own, the one behind CmdStan's optimize. It returns the posterior mode. CmdStan's optimize defaults to jacobian=0, the penalized maximum likelihood, and stanli cannot offer that: the change-of-variables Jacobian is folded into the graph when the model is lowered. jacobian=False raises rather than quietly handing back the other quantity.

Call a Stan function from Python

Function exposes a pure, value-returning Stan user-defined function without building a model or compiling C++. Source compilation happens once:

source = """
functions {
  vector affine(vector x, real a, real b) {
    return a * x + b;
  }
}
model {}
"""
affine = stanli.Function("affine", stan_code=source)
affine(x=[1, 2, 4], a=2.5, b=-1)  # array([1.5, 4.0, 9.0])

Use stan_file="functions.stan" instead of stan_code, or pass mir=stanli.stan_to_mir(source) to reuse cached compilation. Calls take keyword arguments or one mapping, such as affine({"x": x, "a": 2.5, "b": -1}). Scalars return Python float/int; vectors, matrices, and arrays return owned NumPy arrays with the same logical shape. Inputs can be rectangular lists or NumPy arrays, including strided views. The adapter uses typed numeric buffers, not JSON.

Integer inputs must fit Stan's 32-bit integers and promote to real formals. Overloads are selected using argument names, rank, and numeric type; select an ambiguous overload explicitly with a resolved name such as f(real,vector). For empty integer arguments, supply an integer-dtype NumPy array ([] defaults to real). This is the native value-only interpreter, not autodiff: complex, void, RNG, and _lp entry points are outside this interface.

From a repository checkout, compare an installed wheel's steady-state calls with plain Python and NumPy:

python tools/bench_python_function.py

For a development build, stage the Release library in python/stanli/_bin/ and prefix the command with PYTHONPATH=python. The benchmark excludes one-time compilation from call latency, checks the answers first, alternates implementations, and reports medians and IQRs.

Reuse a Function handle across calls: its native function lookup tables are cached at construction. Exact Python float and int arguments use a direct scalar path; NumPy scalars and other array-like values retain NumPy conversion. Overload selection, integer bounds, and shape validation still apply on every call. See the optimization measurements and four-way A/B command for separate measurements of scalar packing and native lookup caching.

How it works

Every Stan model is a composition of a fixed vocabulary of operations: densities, constraint transforms, linear algebra, elementwise math. stanli ships those precompiled and turns each model into data, a static graph of ops over flat preallocated buffers, instead of generating and compiling C++ per model. The graph doubles as the autodiff tape, so a reverse sweep is a backwards loop over an array, and steady-state gradient evaluation allocates nothing.

Two things are not reimplemented, which is what makes the results trustworthy: the compiler is the real stanc3 plus the stanli OCaml pipeline, embedded in the runtime on macOS and Linux and packaged as an executable on Windows, and the math is unmodified stan-math, the same code CmdStan runs.

Correctness

Nothing here ships on "looks close".

118 of 120 posteriordb models are differentially verified against CmdStan: same model, same data, same evaluation point, comparing the log density and every single gradient component. 55 agree bitwise. The worst deviation among these verified posteriordb models is 7.1e-13 relative.

The two exceptions are documented rather than hidden. sir's ODE solution dips about 1e-9 below a declared lower bound at the shared evaluation point, where CmdStan rejects it too; kronecker_gp matches on the log density and 436 of 438 gradients, differing on the two that flow through eigenvectors of a nearly degenerate covariance matrix.

Full per-model accuracy table: docs/corpus-status.md

Performance

In the 2026-10-15 native run,

337 of 343 models produced paired gradient

measurements. The median CmdStan/Stanli ratio was

2.11x, with 320 at or above parity.

Stanli avoids a per-model C++ build and can combine repeated work into fewer runtime operations. Dense kernels and serial dependencies offer fewer such opportunities; the model and data determine the result.

The current full table and method include every model, failed or capped measurement, and the explicit cost-estimate formula. The optimized-reference appendix compares against CmdStan with stanc3 O1 and loop vectorization enabled.

API

The surface is small on purpose.

import stanli

# A path to a .stan file, or the model source directly.
model = stanli.Model(stan_file="model.stan", data="data.json")
model = stanli.Model(stan_code=src, data={"J": 8, "y": y, "sigma": sigma})

model.n_unconstrained               # length of the unconstrained vector
model.constrained_names             # ['mu', 'tau', 'theta[1]', ...]

lp, grad = model.log_prob_grad(q)   # sampling log density and its gradient

fit = model.sample(seed=1, warmup=1000, samples=1000, delta=0.8,
                   refresh=100)
fit["theta"]                        # (chains*draws, 8) ndarray
fit["theta[1]"]                     # one column, chains concatenated

Ctrl-C while sample() runs stops every chain after its current transition and raises KeyboardInterrupt.

Pathfinder can generate one initialization per chain before NUTS. The sampling seed controls both stages; an empty options object uses CmdStan's single-path defaults:

fit = model.sample(
    chains=4,
    seed=303,
    pathfinder_init={"num_iterations": 500, "num_elbo_draws": 25},
)

The other supported options are history_size and Pathfinder's own init_radius. pathfinder_init and explicit inits are mutually exclusive; single-path Pathfinder does not perform PSIS resampling.

data accepts a path to a JSON file or a dict of Python scalars, lists, and numpy arrays. sample returns every column CmdStan's CSV would carry (constrained parameters, transformed parameters, generated quantities, with RNG draws streamed per chain); a theta declared as vector[8] gets columns theta[1] through theta[8], the way CmdStanPy names them. Indexing by the variable name, fit["theta"], returns an array with the declared shape; dot names like theta.1 are still accepted when indexing. Sampler columns (lp__, divergent__, ...) are reachable by name too.

Platforms

Wheels for macOS (arm64 and x86_64), Linux (x86_64 and aarch64, manylinux_2_28) and Windows (x86_64). The Windows wheel is built under mingw-w64, because stan-math does not build under MSVC (the same reason RStan ships through RTools). Windows wheels built from this revision run stanli-compile.exe as a short-lived subprocess and keep pristine stanc.exe beside it for one rollback cycle; there is no OCaml compiler DLL. Python prefers stanli-compile.exe whenever it is present and selects stanc.exe only when it is absent. Launch errors, compiler errors, and empty output are reported; an invalid portable result is rejected when decoded. None causes an automatic retry through stock stanc. The public API works the same way as the embedded compiler path.

The installed library is 29.7 MB: over half of it is the density kernels, about a quarter the embedded stanc3, and the interpreter and NUTS together are about 410 KB. That is the trade this design makes: ship the compiler and every kernel once, so nothing is ever built on the user's machine.

Limits

Stated plainly:

  • The sampler is Stan's own NUTS with diagonal-metric adaptation, and optimize() is Stan's L-BFGS. Single-path Pathfinder is available for sampler initialization; a standalone Python Pathfinder result is not yet exposed.
  • inits are on the unconstrained scale. model.unconstrain({...}) turns constrained starting values into that vector, so unconstraining is a step per starting point rather than a second kind of argument.
  • optimize(jacobian=False) (CmdStan's default penalized maximum likelihood) raises; see above.

What is here is verified against CmdStan model by model, and every number on this page is reproducible from the repository.

Metadata

Release files for stanli 0.21.0

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

Built distributions (wheels)

Table of built distributions (wheels) for stanli 0.21.0
File
stanli-0.21.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
stanli-0.21.0-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details
stanli-0.21.0-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details
stanli-0.21.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
stanli-0.21.0-py3-none-macosx_10_15_x86_64.whl Python 3 none macOS 10.15+ x86-64 Details

Total release size: 73.4 MB

Release files / stanli-0.21.0-py3-none-win_amd64.whl

Download URL stanli-0.21.0-py3-none-win_amd64.whl
Size 20.4 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
1845ad9281ad4acc899c583205ee24629efe646f4b1f1111dc6aa4c69d1ce791
BLAKE2b-256 checksum
How to use checksums
4450241bd7d204f0a16faf5823450afb3dd69b43eecb4b75ffe882596fc2d110
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 16, 2026.

Transparency log

Release files / stanli-0.21.0-py3-none-manylinux_2_28_x86_64.whl

Download URL stanli-0.21.0-py3-none-manylinux_2_28_x86_64.whl
Size 15.0 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
14825e1e072b40ef6641f9e4a779c4a74211319673714c7c25354caeffac8fda
BLAKE2b-256 checksum
How to use checksums
155ff9edbd9f38fe8797b56768aca5fa6adc425a21543d5c70d4935ebb756326
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 16, 2026.

Transparency log

Release files / stanli-0.21.0-py3-none-manylinux_2_28_aarch64.whl

Download URL stanli-0.21.0-py3-none-manylinux_2_28_aarch64.whl
Size 13.6 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
02a1a08b02fbc66489ea10174ba57987c4992a0faf2ff7ad6c7e35482ff5e7a6
BLAKE2b-256 checksum
How to use checksums
72a9bd3472d338d5eeb3f23f6a8306baa8a179ef5c2b0773f8d1902a9e35c9bc
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 16, 2026.

Transparency log

Release files / stanli-0.21.0-py3-none-macosx_11_0_arm64.whl

Download URL stanli-0.21.0-py3-none-macosx_11_0_arm64.whl
Size 10.4 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
7cf05b36f72ff37ade772e379a950d0233d9fbf221671558b1b025dc3100bcd2
BLAKE2b-256 checksum
How to use checksums
481e95b9308601a3f5f914c8561edef0d9f84017f53366af75ab77be2226c02d
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 16, 2026.

Transparency log

Release files / stanli-0.21.0-py3-none-macosx_10_15_x86_64.whl

Download URL stanli-0.21.0-py3-none-macosx_10_15_x86_64.whl
Size 14.0 MB
Tags Python 3 macOS 10.15+ x86-64
SHA-256 checksum
How to use checksums
04f2a83508ee4606759bf87597f045c38e0835f914c8365abd11e96cb2573ac3
BLAKE2b-256 checksum
How to use checksums
c00a726b6a6003c6773797e557832f6e9248cfd45964e994d5b27e33ffea1292
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 16, 2026.

Transparency log

Security for stanli 0.21.0

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.

Full provenance

All 5 files in this release have been uploaded with provenance metadata.

Select from 5 files

Attestation for stanli-0.21.0-py3-none-win_amd64.whl

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 16, 2026.

Transparency log

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.21.0 This release

5 release files Full 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