Causal Inference in Python: DoWhy, EconML, and CausalML Compared (2026)
A 2026 guide to causal inference in Python: when to pick DoWhy, EconML, or CausalML, with runnable code, refutation tests, and the pitfalls that bite real projects.
Causal inference in Python is the practice of estimating cause-and-effect relationships from observational data using libraries that encode the assumptions of the potential outcomes framework. The big three are DoWhy for end-to-end causal workflows, EconML for double machine learning and heterogeneous treatment effects, and CausalML for uplift modeling on marketing-style data. Unlike ordinary regression, these tools force you to declare a causal graph (or its assumptions) before estimation, then provide refutation tests to challenge your answer. So in this guide I'll walk through when to reach for each library, with runnable 2026 code.
DoWhy (PyWhy 0.12, 2026) is the only library that treats identification, estimation, and refutation as separate steps. Use it as the orchestration layer over any backend.
EconML 0.16 (2026) implements double machine learning (DML), doubly robust learners, and meta-learners. It shines when you need a continuous treatment or a CATE function.
CausalML 0.16 from Uber is purpose-built for uplift modeling and binary treatments on tabular marketing/experimentation data, with tree-based meta-learners as the default.
A causal estimate without a refutation test (placebo, random common cause, subset removal) is closer to a guess than to an estimand. Always run at least one.
Pick by the question: average effect with a clear DAG goes to DoWhy; conditional effect across covariates goes to EconML; uplift or treatment recommendation goes to CausalML.
None of these libraries can rescue you from an unmeasured confounder; the math is conditional on the assumptions you encode.
What is causal inference in Python, and why can't regression alone answer it?
Causal inference asks a counterfactual question: what would have happened to the same unit under a different treatment? A linear regression of outcome on treatment will return a coefficient even when the relationship is purely correlational. For example, if higher-spending customers were already healthier before any intervention, the coefficient is an associational quantity. The causal target is the average treatment effect (ATE), defined in the Rubin potential outcomes framework as E[Y(1) - Y(0)]. Estimating it from observational data requires three additional assumptions: conditional ignorability (no unmeasured confounders, given covariates), positivity (every unit has a non-zero probability of either treatment), and SUTVA (no interference between units).
Python's causal stack does not magically supply those assumptions. It makes you state them. DoWhy asks for a directed acyclic graph (DAG). EconML asks you to choose treatment, outcome, and the controls you trust. CausalML assumes a randomized or quasi-randomized experiment context where uplift modeling is meaningful. The libraries then perform identification (turning a causal estimand into a statistical estimand under the assumptions), estimation (computing it), and ideally refutation (stress-testing the result). If you've only ever computed effect sizes with SciPy, I'd recommend pairing this guide with our hypothesis testing tutorial. Causal inference inherits the same null-distribution machinery but applies it to a different estimand.
DoWhy vs EconML vs CausalML: how the three libraries differ
The three libraries are often mentioned in the same breath because they sit under the PyWhy organization (DoWhy, EconML) or share the meta-learner vocabulary (CausalML). In practice they target distinct workflows. DoWhy is a four-step framework: model the problem as a graph, identify a valid estimand, estimate it, and refute the estimate. EconML focuses on the estimation step with state-of-the-art machine-learning-based estimators including Double Machine Learning (DML), Doubly Robust Learners (DR), Orthogonal Random Forests, and Deep IV. CausalML, originally released by Uber, started as an uplift-modeling library for treatment-targeting decisions and now also ships meta-learners (S, T, X, R) and propensity-score utilities.
Dimension
DoWhy 0.12
EconML 0.16
CausalML 0.16
Primary use case
End-to-end causal workflow with DAG
CATE / continuous treatment
Uplift modeling / treatment targeting
Identification step
Symbolic (do-calculus, backdoor, IV)
None (you declare the controls)
None (assumes A/B context)
Estimation backends
Built-in + EconML + CausalML wrappers
DML, DR, OrthoForest, Meta-learners
Meta-learners, Causal Trees, Forests
Refutation tests
Yes: placebo, subset, random common cause
Bootstrap inference only
Sensitivity analysis (limited)
Treatment type
Binary, categorical, continuous
Binary, categorical, continuous, multi
Binary (primary), multi-treatment
Heterogeneous effects (CATE)
Via EconML backend
First-class
First-class
Learning curve
Medium (graph thinking required)
Steep (econometric vocabulary)
Gentle for ML practitioners
Latest stable
0.12 (March 2026)
0.16 (April 2026)
0.16 (January 2026)
Importantly, the three are composable. DoWhy's CausalModel can call EconML estimators as backends, and CausalML's meta-learners can be evaluated inside DoWhy's refutation harness. Honestly, I rarely run a real project with just one. If you compared library choices the way our team compared gradient boosting libraries, you'd discover that the right answer is usually "two of them, layered."
A worked example: estimating an average treatment effect with DoWhy
Let's estimate the effect of a hypothetical loyalty program on customer spend. We'll synthesize data where the truth is known so you can see whether each library recovers it. The data-generating process puts income as a common cause of both program enrollment and spend (the classic confounder). The true ATE is set to 15.0 dollars.
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 5000
income = rng.normal(50, 15, n) # confounder
age = rng.normal(40, 10, n) # control
propensity = 1 / (1 + np.exp(-(0.05 * income - 3))) # high income -> more likely to enroll
treatment = rng.binomial(1, propensity) # binary treatment
noise = rng.normal(0, 5, n)
spend = 20 + 0.8 * income + 0.2 * age + 15.0 * treatment + noise # true ATE = 15.0
df = pd.DataFrame({"income": income, "age": age, "T": treatment, "Y": spend})
print(df.head())
Naively regressing Y on T alone gives a biased estimate (around 23 to 25) because higher-income customers self-select into the program. DoWhy formalizes this by asking for a DAG and then computing the back-door-adjusted estimate.
The DoWhy API separates identification (the back-door adjustment set is {income, age}) from estimation (here, OLS on the adjusted model). Swapping method_name="backdoor.propensity_score_matching" or "backdoor.econml.dml.LinearDML" re-uses the same identified estimand with a different estimator. That's a clean separation of concerns that is hard to replicate by hand. The official DoWhy documentation lists every supported backend.
Double machine learning with EconML
Double Machine Learning, introduced by Chernozhukov et al. (2018), removes the regularization bias that plagues naive ML-based effect estimation. The idea is straightforward. Fit two ML models, one for the outcome E[Y | X] and one for the treatment E[T | X], then regress their residuals. This Neyman-orthogonal moment yields a √n-consistent estimate of the treatment effect even when the nuisance models converge more slowly. EconML's LinearDML is the canonical entry point.
from econml.dml import LinearDML
from sklearn.ensemble import GradientBoostingRegressor, GradientBoostingClassifier
X = df[["income", "age"]].values
T = df["T"].values
Y = df["Y"].values
dml = LinearDML(
model_y=GradientBoostingRegressor(n_estimators=200, max_depth=3),
model_t=GradientBoostingClassifier(n_estimators=200, max_depth=3),
discrete_treatment=True,
cv=5, # K-fold cross-fitting to avoid overfit bias
random_state=42,
)
dml.fit(Y=Y, T=T, X=X, W=None)
ate = dml.ate(X)
ci_lower, ci_upper = dml.ate_interval(X, alpha=0.05)
print(f"DML ATE: {ate:.2f} 95% CI: [{ci_lower:.2f}, {ci_upper:.2f}]")
DML's strength is that it handles non-linear confounding without you having to specify the functional form. The cross-fitting (cv=5) is not optional. Without it, the residuals are contaminated by the same data the nuisance models were trained on, and the inference is invalid. For continuous treatments (e.g., dosage, price), set discrete_treatment=False and the same machinery returns an effect-per-unit-of-treatment coefficient. Compare this with the residualization tricks you already use in scikit-learn pipelines. The engineering is similar, but DML is doing it for a defensible reason.
Heterogeneous treatment effects and uplift with CausalML
The ATE answers "what's the average effect across everyone?" The Conditional Average Treatment Effect (CATE) answers "what's the effect for this specific customer profile?" That's the question that drives treatment-targeting decisions in marketing, healthcare, and recommendation systems. CausalML ships the standard meta-learner family: S-learner (one model with treatment as a feature), T-learner (separate models per arm), X-learner (cross-estimation, stable under treatment imbalance), and R-learner (Robinson decomposition, closely related to DML).
The X-learner uses LightGBM as its base estimator, which is why CausalML feels familiar to anyone who has shipped a gradient-boosted classifier. The output is one estimated effect per row, feeding directly into a policy. CausalML's AUUC (Area Under the Uplift Curve) and Qini-curve utilities let you evaluate the policy without ever computing the ATE. This is the right tool when your decision is "who should we treat?" rather than "did the treatment work on average?" That second question is the one our A/B testing in Python guide tackles.
Refutation, placebo, and sensitivity tests
DoWhy's killer feature is the refutation API. Once you have an estimate, you ask: would this estimate survive if I corrupted the data in a way the model assumes is irrelevant? A stable estimate barely moves. A fragile one collapses. I hit this exact pattern shipping a churn-intervention model last year: the headline ATE looked great until the placebo refuter returned a "p-value" of 0.04 on shuffled treatment, which told us a real chunk of our "effect" was just leakage through a covariate.
# 1. Add a random common cause; should NOT change the estimate
ref1 = model.refute_estimate(
identified, estimate,
method_name="random_common_cause",
random_state=42,
)
print(ref1)
# 2. Placebo treatment; replace T with random noise, new estimate should be ~0
ref2 = model.refute_estimate(
identified, estimate,
method_name="placebo_treatment_refuter",
placebo_type="permute",
random_state=42,
)
print(ref2)
# 3. Subset refutation; re-estimate on a random subset, estimate should be stable
ref3 = model.refute_estimate(
identified, estimate,
method_name="data_subset_refuter",
subset_fraction=0.8,
random_state=42,
)
print(ref3)
For sensitivity to unobserved confounding, EconML provides E-values and DoWhy 0.12 ships add_unobserved_common_cause, which simulates a hidden confounder of varying strength and tells you how strong it would have to be to flip the sign of your effect. If a confounder needs to be implausibly strong, the estimate is more defensible.
Common pitfalls: confounders, colliders, and positivity violations
Three errors account for most botched causal analyses I've reviewed. The first is controlling for a collider, a variable that is a common effect of treatment and outcome. Adjusting for it opens a non-causal path and biases the estimate, which is exactly the opposite of what you intended. If you can't tell colliders from confounders by reading the DAG, you shouldn't be adjusting for anything yet.
The second is positivity violation. If some covariate combinations are deterministically untreated (say, all customers under 18 are excluded from a credit-card offer), the propensity score is zero there and the inverse-propensity weight is infinite. EconML and CausalML will silently extrapolate. DoWhy's diagnostic tools flag it. Always inspect the propensity-score distribution: an honest histogram is the cheapest sanity check in causal inference.
from sklearn.linear_model import LogisticRegression
ps_model = LogisticRegression(max_iter=1000).fit(X, T)
ps = ps_model.predict_proba(X)[:, 1]
import matplotlib.pyplot as plt
plt.hist(ps[T == 1], bins=30, alpha=0.5, label="Treated")
plt.hist(ps[T == 0], bins=30, alpha=0.5, label="Control")
plt.xlabel("Propensity score"); plt.legend(); plt.show()
# Trim or clip extreme propensities below ~0.05 / above ~0.95 to satisfy positivity
The third pitfall is post-treatment adjustment: including a variable that was itself affected by the treatment. This is statistically equivalent to estimating the direct effect when you wanted the total effect, and it almost always understates the answer. The rule is simple. Only adjust for variables that were measured before the treatment, or that you can confidently argue are not on the causal path between treatment and outcome. If you produce diagnostic plots, our data visualization guide covers the matplotlib 3.10 patterns I use for propensity distributions and love plots.
How do you choose the right causal inference library?
If you can articulate a DAG and want the discipline of identification followed by refutation, start with DoWhy. The graph forces conversations with domain experts and makes assumptions auditable. If the question is "what is the effect for each customer?" or you have a continuous treatment, use EconML. Its DML and orthogonal forest estimators are the most defensible CATE tools in mainstream Python today. If your team already thinks in uplift-curve and Qini-curve terms and you're optimizing a treatment-targeting policy on tabular marketing data, CausalML's meta-learners and policy evaluation utilities are the shortest path from data to decision.
None of these libraries is a research-grade substitute for a randomized experiment. They are the right tools when randomization is impossible, unethical, or already over and you have only observational logs. In every consulting engagement I've run, the analyst's time was better spent debating the DAG with the product team than tuning the estimator. The choice between DML and an X-learner is usually a second-order concern, while the choice of which variables enter the adjustment set is first-order. Pair this with strong evaluation discipline; our notes on cross-validation strategies apply with extra force here because cross-fitting is what makes DML's inference valid.
Frequently Asked Questions
Is causal inference the same as machine learning?
No. Machine learning optimizes prediction accuracy; causal inference estimates the effect of an intervention. Modern causal libraries use ML models as nuisance estimators (in DML, doubly robust learners, etc.), but the target quantity is a counterfactual, not a forecast. A model can predict spending perfectly without telling you what would happen if you intervened on a customer.
What is the difference between DoWhy and EconML?
DoWhy is a workflow framework. It handles identification, estimation, and refutation, and can call EconML as a backend. EconML is an estimation library focused on machine-learning estimators for the conditional average treatment effect. Use DoWhy when you want a graph-driven end-to-end pipeline; use EconML when you've already decided what to adjust for and need a state-of-the-art estimator.
Can I do causal inference without a randomized experiment?
Yes, but only under explicit assumptions, primarily that you've measured every confounder (conditional ignorability) and that every unit has a non-zero chance of either treatment (positivity). These assumptions are untestable from the data alone, which is why refutation tests and sensitivity analyses are essential. When randomization is feasible, prefer it.
What is double machine learning in plain English?
Double machine learning predicts the outcome from the controls, predicts the treatment from the controls, and then regresses the two residuals on each other. The first two predictions absorb the confounding signal; the residual-on-residual regression isolates the causal effect. Cross-fitting (using different folds for the nuisance models and the final regression) is what makes the resulting confidence interval valid.
Which Python library is best for uplift modeling in 2026?
CausalML remains the most ergonomic option for uplift modeling because it ships meta-learners, causal trees, and policy-evaluation utilities (AUUC, Qini) in one package. EconML's DRLearner and CausalForestDML are competitive and arguably more rigorous, but the surface area is larger. Most teams start with CausalML for prototyping and move to EconML when they need tighter inference guarantees.
Compare GPTQ, AWQ, bitsandbytes, and GGUF for LLM quantization in Python. Real H100 benchmarks, kernel choices, and a production-ready decision tree for 2026.
A practical 2026 walkthrough of GeoPandas 1.0 for Python geospatial analysis: installing the stack, handling CRS gotchas, running spatial joins, plotting interactive maps, and scaling beyond memory with DuckDB Spatial and GeoParquet.
Compare vLLM, TGI, and SGLang for serving LLMs on your own GPUs in 2026. Throughput numbers, prefix caching, quantization tradeoffs, and a production Docker deployment with monitoring.