pytest for economic logic.
EconEval is a small open-source framework for pytest-style checks on economic logic in CI.
It catches problems unit tests often miss: broken rules, drift after data changes, and outputs that stop making economic sense.
Latest release: v0.5.2
EconEval currently does three things:
- loads a model check config
- runs invariant tests against a Python object
- writes JSON, JUnit, Markdown, or HTML reports that CI can keep or fail on
The config layer is validated with Pydantic, and invariant evaluation uses a static AST pre-check plus a numeric numexpr fast path for safe math-heavy expressions.
Use it to:
- check that important economic rules still hold
- make model assumptions explicit in code
- fail pull requests when a change breaks a rule you care about
The invariant engine, CLI, reports, examples, and GitHub Action are usable today. The R, Stata, and Julia bridge helpers are also available for teams that want to call the CLI from other ecosystems.
The broader checks are still evolving:
- stress testing
- fairness checks
- drift checks
- PDF reports
That split is intentional: the core is meant to be small, auditable, and easy to run in CI.
EconEval is for people who want a lightweight, CI-native way to check whether a model still makes economic sense. The core stays close to unit-test style validation; the broader checks are extensions on top of that.
- academic economists validating research code and replication projects
- policy analysts checking that a model still respects program rules and constraints
- quantitative consultants delivering models to clients with auditability requirements
- teams working on forecasting, scenario analysis, or simulation pipelines
- researchers who want a lightweight validation layer before a model is published or deployed
- applied data scientists building models that need economics-aware guardrails
- analysts who want CI checks they can explain in a report or appendix
Traditional software tests are useful, but they do not tell you whether a model still behaves like a valid model.
For example, a change might:
- flip the sign of an elasticity
- violate a market-clearing condition
- break a policy constraint
- quietly change the meaning of a downstream output
EconEval gives you a place to encode those rules and run them automatically.
The first usable version of EconEval does three things well:
- read a simple YAML config
- evaluate invariant expressions against a model object
- return a clear pass or fail result that GitHub Actions can use
That is enough to support a real workflow without pretending to solve every validation problem at once.
EconEval is intentionally not a full Python sandbox.
- the expression syntax is deliberately restricted
- the YAML-like parser supports the documented config shape, not every YAML feature
- fairness, drift, PDF reports, and non-Python bridges are evolving extensions
That keeps the core small and auditable, but it also means EconEval is not meant to replace general-purpose application logic.
Run a passing elasticity check:
econeval --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report out/pass.jsonThe rule is model.elasticity < 0, and DemoModel passes because its elasticity is negative.
Run the same rule against a failing model:
econeval --config examples/broken_model/econeval.yml --model examples/broken_model/model.py --class BrokenModel --report out/fail.jsonBrokenModel fails because its elasticity is positive.
project: demo-model
version: 1
invariants:
- name: elasticity_must_be_negative
expression: model.elasticity < 0
- name: supply_must_be_non_negative
expression: model.supply >= 0
stress_tests:
- name: stagflation_shock
dataset: data/stagflation.csv
metric: mape
threshold: 0.15
- name: macro_stagflation
kind: synthetic
metric: invariants
manipulations:
- variable: input_data.unemployment_rate
action: add
value: 0.04
- variable: input_data.energy_costs
action: multiply
value: 1.5
invariants:
- name: slowdown_flag
expression: model.predicted_gdp_growth < 0.01
fairness:
enabled: true
metrics:
- demographic_parity_difference
- disparate_impact_ratio
- gini
- atkinson
- equal_opportunity_difference
- equalized_odds_differenceflowchart LR
A[Model repo] --> B[econeval.yml]
B --> C[Load config]
C --> D[Run checks]
D --> E[Collect results]
E --> F{Pass or fail CI}
econeval/
.pre-commit-config.yaml
.github/
workflows/
ci.yml
action.yml
bridges/
julia/
econeval_bridge.jl
r/
econeval_bridge.R
stata/
econeval_bridge.do
scripts/
precommit_econeval.py
examples/
basic_model/
model.py
econeval.yml
csv_model/
README.md
econeval.yml
drift_model/
model.py
econeval.yml
fairness_model/
README.md
model.py
econeval.yml
broken_model/
model.py
econeval.yml
policy_model/
model.py
econeval.yml
advanced_model/
model.py
econeval.yml
src/
econeval/
__init__.py
cli.py
config.py
invariants.py
traceability.py
scenarios.py
reporting.py
tests/
test_config.py
test_invariants.py
For development from a checkout:
git clone https://github.com/Farukhsb/econeval.git
cd econeval
pip install -e .[dev]pytest and ruff are included in the dev extra. Install without the extra if you only want the CLI.
If you want the repo-local pre-commit hook that runs EconEval on the basic example, install the hooks once:
pre-commit installEconEval parses its YAML-like config format with its own loader, so you do not need PyYAML for the current release.
For a public release install from PyPI:
pip install econeval==0.5.2If you want the exact release state, install from the tagged GitHub release:
pip install git+https://github.com/Farukhsb/econeval.git@v0.5.2If you want to inspect the release artifacts first, start from the v0.5.2 tag or the GitHub release page.
- Install the package with
pip install econevalorpip install -e .[dev]from a checkout. - Run the basic example config.
- Check the generated report and try the broken example next.
Example:
econeval --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.jsonIf you want a quick sanity check, run the broken example next and confirm it fails.
To compare the large-array expression backends, run:
python benchmarks/numexpr_benchmark.py --size 100000 --iterations 20Create a config file that lists the checks you want to enforce, then point EconEval at a Python model class.
Command line example:
econeval --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.jsonpython -m econeval works the same way. For iterative development, --watch reruns the checks when the config or model file changes:
econeval --watch --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.jsonFor extra traceability on failed invariants, add --blame. EconEval will try to run
git blame on the model lines that look related to the failure and include the
commit metadata in the report.
econeval --blame --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.jsonFor model-less CSV relation checks, you can omit --model when the config only contains kind: relation stress tests:
econeval --config examples/csv_model/econeval.yml --report econeval-report.jsonTo compare a current run against a previous JSON report, pass --baseline-report:
econeval --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.json --baseline-report baseline-report.jsonTo write a text report instead:
econeval --config examples/advanced_model/econeval.yml --model examples/advanced_model/model.py --class AdvancedModel --report econeval-report.md --format markdown
econeval --config examples/advanced_model/econeval.yml --model examples/advanced_model/model.py --class AdvancedModel --report econeval-report.html --format htmlTo write a PDF report, install the optional extra first:
pip install econeval[pdf]
econeval --config examples/advanced_model/econeval.yml --model examples/advanced_model/model.py --class AdvancedModel --report econeval-report.pdf --format pdfIf you need a custom economic check kind, register a handler in Python and then
use that kind in your config:
from econeval.config import EconomicCheck
from econeval.scenarios import register_economic_check_handler
def run_custom_check(model, check):
return check.name, check.kind
register_economic_check_handler("custom_policy_check", run_custom_check)The repository also exposes a composite GitHub Action so workflows can run EconEval in one step.
- uses: Farukhsb/econeval@v1
with:
config: examples/basic_model/econeval.yml
model: examples/basic_model/model.py
class: DemoModel
report: econeval-report.json
python-version: "3.11"The action installs the package from the action source, sets up Python, and runs the CLI with the inputs you provide.
Fairness checks are available, but they are still an extension rather than the core path.
They expect a tabular dataset with:
- a group column, defaulting to
group - one or more feature columns that are passed to
predict(features) - an
actualcolumn when you use label-based metrics such asequal_opportunity_differenceorequalized_odds_difference
The example in examples/fairness_model/README.md shows the full data format.
The built-in thresholds are:
demographic_parity_difference: pass at<= 0.2disparate_impact_ratio: pass at>= 0.8equal_opportunity_difference: pass at<= 0.2equalized_odds_difference: pass at<= 0.2gini: pass at<= 0.3atkinson: pass at<= 0.2
Fairness results also include a severity field:
passfor checks within the thresholdwarnfor borderline misses that should not fail CIfailfor clear misses or execution errors
To run the full advanced example with synthetic shocks, drift checks, fairness metrics, and scan checks:
econeval --config examples/advanced_model/econeval.yml --model examples/advanced_model/model.py --class AdvancedModel --report artifacts/advanced-report.md --format markdownWhat the runner expects:
- a model file that defines a class you can import by name
- a
predict(features)method for stress tests, drift checks, and fairness checks - CSV datasets with an
actualcolumn for stress tests - CSV datasets with the feature or group columns required by the check
EconEval can also adapt common tabular estimators directly when they expose feature metadata such as feature_names_in_ or exog_names.
If your runtime looks different, use a thin adapter. EconEval also handles common shapes like callable models, solve() wrappers, and PyMC-style posterior predictive samplers, so you can bridge external engines without rewriting the check pipeline.
Example invariant rule:
- name: elasticity_must_be_negative
expression: model.elasticity < 0If the expression returns False, the invariant fails.
The JSON report includes the project name, a summary count, and the result of each invariant, economic check, stress test, drift check, economic drift check, fairness check, and optional baseline comparison block.
EconEval uses a restricted invariant engine with a static AST pre-check.
The supported path is intentionally narrow:
- comparisons, boolean logic, simple arithmetic, and attribute access on the model object
- numeric/vectorized expressions through
numexprwhen the expression is safe for that backend
It rejects function calls, subscripts, comprehensions, lambdas, dictionaries, sets, tuples, lists, and private attributes such as __class__.
That design keeps the syntax simple for users while avoiding raw eval() and other arbitrary Python execution paths.
The expression layer is intentionally not a general Python runtime.
That is a design choice:
- keep invariant checks auditable
- make failures easier to explain in CI
- avoid hidden side effects and sandbox escape risks
- keep the supported syntax small enough that users can reason about it quickly
The practical rule is simple: if a check needs full Python, put that logic in code, not in the expression string. The expression engine is meant for declarative rules, not imperative workflows.
examples/basic_modelshows the happy path.examples/csv_modelshows model-less CSV relation checks.examples/broken_modelshows a model and dataset that fail the checks.examples/drift_modelfocuses on drift validation, including PSI, trend drift, and regression drift over time.examples/fairness_modelfocuses on fairness checks and a simple stress test.examples/policy_modelis a compact policy example with fairness, invariants, and a monotonicity check.examples/tax_policy_simulatorshows a small Adult-derived tax policy model with liability and rate checks.examples/advanced_modelshows accounting identities, monotonicity, convergence, grid sweeps, synthetic shocks, and GitHub-friendly report output.--baseline-reportcompares a current report against a prior JSON run and highlights regressions, improvements, and new or removed checks.examples/demo_notebook.ipynbis a short walkthrough you can open in Jupyter or VS Code.- The repository examples are intended to double as a lightweight demo workflow.
The first bridge helper is a thin R wrapper in bridges/r/econeval_bridge.R.
It shells out to the EconEval CLI, so R users can keep their model code in R and still run EconEval checks in CI or locally.
There is also a Stata wrapper in bridges/stata/econeval_bridge.do.
There is also a Julia wrapper in bridges/julia/econeval_bridge.jl.
- richer traceability and failure explanations
- deeper drift comparison and alerting
- richer fairness configuration and reporting
- more real-world examples and benchmark coverage
- deeper interop with tools like
PyMC,GAMS, and Julia - optional
statsmodels-based drift helper viaeconeval[stats] - dashboard polish with filtering and collapsible drill-downs
Publishing a GitHub Release triggers the release workflow in .github/workflows/release.yml.
That workflow:
- installs the package
- runs the test suite
- runs EconEval against the example model
- uploads a release report artifact
- a richer report viewer
- more scenario types
- baseline trend summaries over multiple releases
When you are ready to publish a new version:
- run the test suite locally
- update the package version if needed
- tag the release, for example
v0.5.2 - publish the GitHub Release so the release workflow runs
- confirm the release artifact uploaded from Actions
- confirm the wheel and sdist were published to PyPI
To publish to PyPI through GitHub Actions, enable PyPI trusted publishing for this repository and then publish the GitHub Release. The release workflow will build the distribution and upload it automatically.
See CHANGELOG.md for version-by-version changes.
MIT