CausalPy is a PyMC toolbox for quasi-experiments, and it tells you which designs it will not do for you
A Python package for causal inference in quasi-experimental settings
At a glance
- What is it?
- A Python package that wraps difference-in-differences, synthetic control, regression discontinuity, interrupted time series and instrumental variables in a Bayesian estimation layer, with a section on when the library is not a fit. The runtime notes are the most valuable part of the documentation, because they name three ways a fresh install can behave differently from what you expect.
- Who is it for?
- Adopt CausalPy if you have a plausible quasi-experimental design and you want uncertainty quantified with a Bayesian model rather than a point estimate and a standard error. Do not adopt it for causal discovery from observational data, for a black box that returns an answer without assumptions, or as a production pipeline tool, because the README names all three as cases where the library is not a fit.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The library starts by naming the designs it does not cover
CausalPy is a Python package for causal inference in quasi-experimental settings, built and maintained by PyMC Labs, and it is described as research-grade causal inference workflows for quasi-experimental designs in Python.
The framing is narrow on purpose. The three headline features are quasi-experimental methods, which are difference-in-differences, synthetic control, regression discontinuity, interrupted time series, instrumental variables and more; Bayesian-first estimation via PyMC with full uncertainty quantification, plus traditional OLS via scikit-learn; and decision-ready outputs, meaning effect summaries with credible intervals, practical significance measured by ROPE, and publication-quality plots.
What makes it useful is that it estimates these designs in a probabilistic model rather than a regression. A difference-in-differences estimate from a Bayesian model comes with a posterior over the effect, and the ROPE, the region of practical equivalence, turns that posterior into a statement about whether the effect is large enough to matter rather than merely distinguishable from zero. That is a different deliverable from a confidence interval, and for the kind of decision where a lift is worth rolling out it is the more useful one.
The section on when CausalPy is a good fit asks for three things: a plausible quasi-experimental design such as a threshold rule, a policy change, a staggered rollout or geo lift; a desire for uncertainty-aware estimates and diagnostics rather than only point estimates; and a need for reproducible analysis artifacts for review and communication.
Then there is the section on when it is not a fit, and it is blunt. You need causal discovery from weakly identified observational data. You want fully automated black box causal answers without specifying assumptions. You primarily need production workflow tooling such as pipelines, governance and multi-user collaboration.
That last one matters most for engineers. CausalPy is an analysis library, not a platform, and the difference is the difference between a notebook you can reproduce and a service you can operate.
The quickstart is a regression discontinuity in twenty lines
The quickstart example is short enough to read in a minute, and it shows the shape of the API.
import causalpy as cp
import matplotlib.pyplot as plt
# Import and process data
df = (
cp.load_data("drinking")
.rename(columns={"agecell": "age"})
.assign(treated=lambda df_: df_.age > 21)
)
# Run the analysis
result = cp.RegressionDiscontinuity(
df,
formula="all ~ 1 + age + treated",
running_variable_name="age",
model=cp.pymc_models.LinearRegression(),
treatment_threshold=21,
)
# Visualize the causal effect at the threshold
fig, ax = result.plot()
# Get a results summary with posterior estimates
result.summary()The design is the minimum drinking age example, and the mechanics are the interesting part. The data comes from `cp.load_data`, which loads a bundled dataset, and the `drinking` name is one of the package's example sets, with the CSVs shipped as package data under `causalpy.data`.
The treatment is assigned with a plain pandas `.assign`, which makes the identifying assumption visible in the data rather than hidden in the library. The `treated` flag is a deterministic function of `age`, and the threshold of 21 is the same number in the assignment and in the model, so a reader can check the identification logic by eye. That transparency is a deliberate property of the design, and it is the reason the formula is passed as a string with the model specified separately as `cp.pymc_models.LinearRegression()`.
The output is an object, not a number. `result.plot()` visualises the regression discontinuity design, showing the estimated jump at the treatment threshold, and `result.summary()` prints posterior estimates of the causal effect with uncertainty intervals. The result object is what you keep, and it is what you hand to a reviewer.
The template files are shipped as package data too, under `causalpy.templates` with `*.html`, which is how the report and the diagnostic plots are produced.
The runtime notes name three ways a fresh install surprises you
The runtime notes section is the most valuable part of the README for anyone actually running this, and it documents three behaviours that are not obvious from the code.
As of the PyMC 6 release, CausalPy runs on PyMC 6, PyTensor 3 and ArviZ 1.x, and requires Python 3.12 or newer. The first note is that numba is now a transitive hard dependency and the default backend. PyTensor 3 compiles models with numba by default, so the first sample from a freshly defined model incurs a noticeable one-off compilation delay before sampling starts, while subsequent samples reuse the compiled function.
That is the benign one. The second is that native crashes can replace Python tracebacks. When a numba-compiled sampler worker dies, the failure may surface as a native crash or a stalled worker rather than a Python traceback, so a wedged worker can present as a silent hang rather than an exception. For a library whose main job is running a sampler for a long time, that is a genuinely important operational fact, because a silent hang is much harder to diagnose than a traceback.
The third is the sharpest. Installing `nutpie` changes sampling defaults. If a recent enough nutpie is installed, PyMC uses it as the default NUTS sampler unless you pass an explicit `nuts_sampler=`, and nutpie applies its own number of tuning steps rather than the PyMC built-in default of 1000. Merely installing nutpie is therefore enough to change how `pm.sample()` behaves, and the fix is to pass `nuts_sampler="pymc"` if you need the built-in sampler.
That is a good example of a dependency changing results without any code change on your side. A conda environment where someone installs a package for an unrelated reason can change your posterior.
The full list of breaking changes for the release is in the GitHub release notes, and the current version is 1.0.0rc1, a release candidate published on 2026-10-06, after 0.9.0 from 2026-07-28 and 0.8.1 from 2026-05-15. The last push to the repository was on 2026-10-15 with the repository not archived.
The methods are mapped to decision contexts, not to academic names
The methods table is organised by decision context rather than by technique, which is a better way to choose a method than the textbook taxonomy.
For focussed testing on certain units such as geographies or products, the options are synthetic control and geographical lift. For evaluating before and after changes, launches and policy changes, the options are differences in differences, staggered DiD and interrupted time series. For exploiting cutoff rules and score-based eligibility such as credit or age, the options are regression discontinuity and regression kink. And for the case where you cannot randomise but need to correct for selection, instrumental variables and inverse propensity score appear in the list.
Organising by decision context is the practical choice, because the method follows from the decision you are trying to make rather than from the estimator you happen to know. Someone asking whether a policy change worked is looking for the second row, and the first row is for someone deciding whether to roll out in one region.
The synthetic control entry is worth a note for anyone in the platform or growth world. Synthetic control builds a weighted combination of untreated units to approximate the treated unit, and it is the method most used for geo experiments where you cannot randomise at the region level. The README lists geographical lift alongside it, which is the commercial measurement variant of the same idea.
Behind the public API, the package depends on the wider scientific Python stack. The dependency list starts with `arviz` at 1.1 or newer for the posterior diagnostics and plotting, `matplotlib` for the figures, `pandas` and `numpy` for the data handling, `graphviz` for the causal graph rendering, `jinja2` for the templated reports, `narwhals` for dataframe interoperability, and `ipython` for the interactive use. The package data configuration shows the shape of the codebase, with `causalpy.data` holding the CSV datasets and `causalpy.templates` holding the HTML templates, and the package discovery excluding `causalpy.test*` and `docs*` from the distribution.
The project also ships agent skills that teach AI coding assistants how to use the library for causal inference, available through the Decision AI Hub and living in `causalpy/skills/` in the source tree, alongside an `AGENTS.md` and an `ARCHITECTURE.md` at the repository root.
A commercial lab ships the library and sells the consulting
There is a line in the README between the library and the business, and it is worth reading carefully.
The README carries a call to action to book a consulting call, and it says CausalPy is built and maintained by PyMC Labs. If your team is exploring a consulting engagement for lift testing, complex or high-stakes causal work, you can book an introductory call. And it says explicitly that these calls are for consulting inquiries only, and that for technical usage questions and free community support you should use GitHub Discussions and the documentation.
So the model is a supported open source library from a company whose business is applied causal work, which is a common and generally healthy arrangement. The library is Apache-2.0 licensed, so you can use it commercially and modify it, and the commercial relationship is optional.
The support channels are split in a way that tells you where each kind of question goes. Usage questions go in GitHub Discussions Q&A, so the issue tracker stays focused on bugs and enhancements. Bugs and feature requests go in Issues. Detailed guides are in the documentation at causalpy.readthedocs.io.
The build and test infrastructure in the Makefile shows how seriously the project takes correctness, and the target names are the interesting part. Alongside the usual init, setup, lint, typecheck, test and docs targets there are `test-nightly`, `test-correctness` and `test-patch-cov`, plus `check-exports` and `check-architecture`.
`check-architecture` is unusual and worth a moment. It implies the package structure is enforced rather than merely documented, which is what keeps a library like this from turning into a single module over time.
`test-patch-cov` comes with a long comment explaining a subtle problem. Patch coverage has to be measured against the branch the pull request actually targets, and while the 1.0 transition branch exists, work branched from it targets that branch rather than `main`, which is hundreds of commits ahead, so comparing against `main` would report the whole migration as the patch and the gate would stop meaning anything. The Makefile even works out which branch to compare against by checking the git refs and the merge base, with a `DIFF_COVER_COMPARE_BRANCH` override for the case where a pull request deliberately targets something else.
That is a small but telling piece of engineering. A coverage gate that measures the wrong diff is worse than no gate, and this one detects the situation and corrects for it. The repository also has a `uv.lock`, an `environment.yml` for conda, a `codecov.yml`, a `scripts/` directory, a `docs/` directory and a `.pre-commit-config.yaml`, and the package declares itself as Development Status 4, Beta, aimed at Science and Research.
Editorial conclusion
Adopt CausalPy if you have a plausible quasi-experimental design and you want uncertainty quantified with a Bayesian model rather than a point estimate and a standard error. Do not adopt it for causal discovery from observational data, for a black box that returns an answer without assumptions, or as a production pipeline tool, because the README names all three as cases where the library is not a fit. Verify first that your design maps to one of the supported methods, that you can state the identifying assumptions explicitly, and that the sampler you end up with is the one you intended, since installing a recent nutpie silently changes the default NUTS sampler and its tuning steps.
Frequently asked questions
Which causal inference methods does CausalPy support?
Difference-in-differences, staggered DiD, synthetic control, geographical lift, regression discontinuity, regression kink, interrupted time series and instrumental variables, among others. The methods are mapped to decision contexts in a table in the README.
Why is my first CausalPy sample slow and can it crash without a traceback?
PyTensor 3 compiles models with numba by default, so the first sample from a newly defined model incurs a one-off compilation delay that later samples reuse. And when a numba-compiled sampler worker dies, the failure can surface as a native crash or a stalled worker rather than a Python traceback, so a wedged worker can look like a silent hang.
What happens if I install nutpie in my environment?
PyMC then uses nutpie as the default NUTS sampler unless you pass an explicit nuts_sampler, and nutpie applies its own number of tuning steps rather than the PyMC default of 1000. Pass nuts_sampler="pymc" to keep the built-in sampler.
When is CausalPy not the right tool?
The README says it is not a fit for causal discovery from weakly identified observational data, for fully automated black box causal answers without specifying assumptions, or when you primarily need production workflow tooling such as pipelines, governance and multi-user collaboration.
How do I install CausalPy?
Run pip install CausalPy, or conda install causalpy -c conda-forge, or install from GitHub with pip install git+https://github.com/pymc-labs/CausalPy.git. It requires Python 3.12 or newer and runs on PyMC 6, PyTensor 3 and ArviZ 1.x.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pymc-labs-causalpy)Was this useful?