stanli
The Stan Language Interpreter. Compile and sample Stan models with no C++ toolchain on the machine.
Install
pip install stanli
That is the whole install. There is no compiler, no make, no CmdStan
checkout, and no first-run build. The wheel is about 11 MB and holds one
shared library with the Stan compiler, stan-math and the sampler.
Quick start
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_code= takes the model source as a string. data is a path to a JSON
file or a dict of Python scalars, lists and NumPy arrays.
Stan #include directives search the directory containing stan_file. Add
shared directories with include_paths; they are searched in order, then the
file's directory (the current directory for stan_code):
model = stanli.Model(stan_file="models/model.stan", data="data.json",
include_paths=["shared", "vendor/stan"])
A constructed model keeps its compiled includes, so later sampling does not reread the files.
Model(..., fast_math=True) opts in to
fast mode:
faster gradients, with results that can differ from the default in the last
bits.
Sampling and diagnostics
Four chains run in parallel by default. R-hat needs more than one chain. Each chain owns its executor and RNG stream, so the draws are byte-identical to a sequential run. Eight schools runs all four chains in about 70 ms.
Sampling prints CmdStan-style progress every 100 transitions, then per-chain 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)
refresh=0 silences it; another positive integer changes the interval.
Output goes through sys.stdout, so notebooks and
contextlib.redirect_stdout() work. Progress never changes draws or RNG
streams. If any post-warmup transition diverges or hits the maximum treedepth,
the final output reports the count. Ctrl-C stops every chain after its current
transition and raises KeyboardInterrupt.
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 match
stansummary.
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.
E-BFMI catches a badly explored heavy tail that R-hat and ESS both miss.
fit.divergences, fit.max_treedepth_hits, fit.stepsize and fit.ebfmi()
are per-chain arrays, and fit.to_arviz() returns an ArviZ InferenceData with
the sampler statistics attached.
Metric and step size
metric is "diag_e" (the default), "dense_e" or "unit_e", as in CmdStan.
inv_metric sets the starting inverse metric: shape (n,) for "diag_e" or
(n, n) for "dense_e", shared by every chain, or one per chain. step_size
sets the initial step size (default 1). adapt_engaged=False turns adaptation
off, so the metric and step size stay as given.
After sampling, fit.metric, fit.step_size and fit.inv_metric hold what
each chain adapted. A pilot fit can start a later one:
pilot = model.sample(seed=1, metric="dense_e")
fit = model.sample(seed=2, metric="dense_e", warmup=0, adapt_engaged=False,
inv_metric=pilot.inv_metric,
step_size=float(pilot.step_size.mean()))
With fewer than 150 warmup iterations Stan does not adapt the metric, and a supplied one is kept.
Within-chain parallelism
For models using reduce_sum or reduce_sum_static,
model.sample(threads_per_chain=4, parallel_chains=2) runs eligible
reductions on up to eight threads. Small or unsupported reductions stay
serial; model.reduce_sum_count and model.reduce_sum_fallbacks report what
was kept. Changing the thread count rebuilds the model and can change rounding
and therefore the draws. See
native reductions.
Other algorithms
optimize() runs Stan's L-BFGS and returns the posterior mode:
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
pathfinder_init fits single-path Pathfinder and draws one NUTS start per
chain from it. The sampling seed controls both stages, and {} uses CmdStan's
defaults:
fit = model.sample(chains=4, seed=303,
pathfinder_init={"num_iterations": 500, "num_elbo_draws": 25})
The other options are history_size and Pathfinder's own init_radius. It
cannot be combined with inits, and a single path does no PSIS resampling.
Standalone Pathfinder, ADVI and Laplace draws are available from the R package only.
Call a Stan function from Python
Function calls a pure, value-returning Stan user-defined function without
building a model. The source compiles 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= instead of stan_code, or pass
mir=stanli.stan_to_mir(source) to reuse a compilation. Calls take keyword
arguments or one mapping. Scalars come back as float or int; vectors,
matrices and arrays come back as NumPy arrays with the declared shape.
Integers must fit Stan's 32-bit range. Select an ambiguous overload by its
resolved name, such as f(real,vector), and pass an integer-dtype array for an
empty integer argument. Complex, void, RNG and _lp functions are not
callable this way. Reuse a Function handle across calls; its lookup tables
are built once.
API reference
import stanli
model = stanli.Model(stan_file="model.stan", data="data.json")
model = stanli.Model(stan_code=src, data={"J": 8, "y": y, "sigma": sigma},
include_paths=None, fast_math=False)
model.n_unconstrained # length of the unconstrained vector
model.constrained_names # ['mu', 'tau', 'theta[1]', ...]
model.unconstrain({"mu": 0.5}) # constrained values -> unconstrained vector
lp, grad = model.log_prob_grad(q) # log density and gradient at q
fit = model.sample(chains=4, seed=1, warmup=1000, samples=1000, delta=0.8,
max_depth=10, thin=1, save_warmup=False, inits=None,
init_radius=2.0, pathfinder_init=None,
parallel_chains=None, refresh=100, threads_per_chain=1,
metric="diag_e", inv_metric=None, step_size=None,
adapt_engaged=True)
fit["theta"] # (chains*draws, 8), declared shape
fit["theta[1]"] # one column, chains concatenated
fit["lp__"] # sampler columns by name too
fit.summary(); fit.diagnose(); fit.to_arviz()
r = model.optimize(seed=1, iter=2000, init=None, init_radius=2.0)
f = stanli.Function("name", stan_file="functions.stan")
mir = stanli.stan_to_mir(src, include_paths=None, fast_math=False)
bs = stanli.bridgestan_model(stan_file="model.stan", data="data.json")
sample returns every column CmdStan's CSV would carry: parameters,
transformed parameters and generated quantities, named the way CmdStanPy
names them (theta[1] through theta[8] for a vector[8] theta). Chain c
uses CmdStan's RNG stream for (seed, c + 1).
inits are on the unconstrained scale: one vector for every chain, or one row
per chain. Pass constrained values through model.unconstrain() first.
init_radius=0 starts every chain at the origin, CmdStan's init=0.
Platforms
Wheels for macOS (arm64, x86_64), Linux (x86_64, aarch64, manylinux_2_28) and
Windows (x86_64). The Windows wheel is built with mingw-w64, because stan-math
does not build under MSVC (RStan ships through RTools for the same reason).
On Windows the compiler runs as a short-lived stanli-compile.exe subprocess.
Limits
optimize(jacobian=False), CmdStan's default penalized maximum likelihood, raises. The change-of-variables Jacobian is part of the compiled graph, sooptimize()always returns the posterior mode.- Standalone Pathfinder, ADVI, Laplace and fixed-parameter sampling are in the R package only.
- Standalone generated quantities over an earlier fit's draws are not available.
More
- How it works: docs/how-it-works.md.
- Correctness: 118 of 120 posteriordb models match CmdStan's log density and every gradient component, 55 of them bitwise, with a worst relative error of 7.1e-13. See TESTING.md.
- Speed: in the 2026-10-18 run, 337 of 343 models produced paired gradient timings. The median gradient is 2.10x faster than CmdStan's, and 321 are at least as fast. See docs/benchmarks.md.
- Source and issues: github.com/seantalts/stanli. License: BSD-3-Clause, matching Stan's.
Metadata
Release files for stanli 0.22.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| stanli-0.22.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| stanli-0.22.0-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details |
| stanli-0.22.0-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details |
| stanli-0.22.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| stanli-0.22.0-py3-none-macosx_10_15_x86_64.whl | Python 3 | none | macOS 10.15+ x86-64 | Details |
Total release size: 72.9 MB
Release files / stanli-0.22.0-py3-none-win_amd64.whl
| Download URL | stanli-0.22.0-py3-none-win_amd64.whl |
|---|---|
| Size | 20.3 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
02cc5124a0ef0d33116360a97415464f48bde344fb5b2d8c3ca34eeac5573055
|
|
BLAKE2b-256 checksum How to use checksums |
dc265ae75635c7a7ca7fab3a58f45fac83e0fe22634f57a6c2a2eb88355464c7
|
| 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 18, 2026.
Transparency logRelease files / stanli-0.22.0-py3-none-manylinux_2_28_x86_64.whl
| Download URL | stanli-0.22.0-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 14.9 MB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
4a66a00d7aba2e9986ce5ac32a829d0c8861c2a732073bd0d57f05c1c0fe532f
|
|
BLAKE2b-256 checksum How to use checksums |
e50ca9f63a688e6513fe2c2ba955b9ba35c30515003822788151cc60c2331aab
|
| 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 18, 2026.
Transparency logRelease files / stanli-0.22.0-py3-none-manylinux_2_28_aarch64.whl
| Download URL | stanli-0.22.0-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 13.5 MB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
47853489893d3b4e5b0b7bdb65cd7595d2646cd29f1acf30d75e55aca255c7b4
|
|
BLAKE2b-256 checksum How to use checksums |
7ef7295ba3d555b2c8b8eed4c6a212679e96ba59cace32c07ad944ffbb9dc17d
|
| 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 18, 2026.
Transparency logRelease files / stanli-0.22.0-py3-none-macosx_11_0_arm64.whl
| Download URL | stanli-0.22.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 10.3 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
d40ffb591d203a52838e0ff3d173c1f6281d18722dd726889575f297f70483b8
|
|
BLAKE2b-256 checksum How to use checksums |
66e989fc0c70561a45781dcba8d7c8241f785d6d5d63ce5b5af0979dd3291d11
|
| 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 18, 2026.
Transparency logRelease files / stanli-0.22.0-py3-none-macosx_10_15_x86_64.whl
| Download URL | stanli-0.22.0-py3-none-macosx_10_15_x86_64.whl |
|---|---|
| Size | 13.9 MB |
| Tags | Python 3 macOS 10.15+ x86-64 |
|
SHA-256 checksum How to use checksums |
7cd0a869928e413b30b85c561b113a5f0eaf46062418ec29f250ba45b29e0ad0
|
|
BLAKE2b-256 checksum How to use checksums |
b57e39a89bfa61abbdbd480b42127f62534089ac83797aed9dcbc75e96df7dbc
|
| 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 18, 2026.
Transparency logSecurity for stanli 0.22.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.
Attestation for stanli-0.22.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 18, 2026.
Transparency logA 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.