modeltest
If you test your code, why not your model?
modeltest is a unit-testing framework for machine learning models. It lets you
define contracts for model quality, robustness, fairness, and data invariants,
and run them automatically in your CI/CD pipeline — just like pytest for code.
Quick start
from modeltest import ModelSuite
from modeltest.scenarios import MinimumAccuracyTest, GroupPerformanceTest
suite = ModelSuite(name="Fraud Detection")
suite.add_test(MinimumAccuracyTest(threshold=0.85))
suite.add_test(GroupPerformanceTest(metric="accuracy", threshold=0.8, group_col="gender"))
result = suite.run(model, X_val, y_val, model_name="fraud_rf")
print(result.report(style="table"))
CLI
After training, validate a model contract from the command line:
# Declarative suite (recommended)
modeltest validate \
--suite suite.yaml \
--model model.pkl \
--data validation.csv \
--target target \
--train-data train.csv \
--output report.xml
# Python suite (suite.py exposing `suite`)
modeltest validate --suite suite.py --model model.pkl --data validation.csv --target target
Extras:
--output report.xmlwrites JUnit XML for CI reporters.--train-dataenables the drift tests (they compare train vs. validation distributions).- Exit code is
1if any test fails,0otherwise.
Prediction caching
Within a single suite.run(...), predictions are computed once and reused
across every test. TestContext.predict() caches by a content hash of the
input, so tests predicting on the same data (MinimumAccuracyTest,
GroupPerformanceTest, the fairness tests, ...) each reuse the result instead
of re-running the model. Perturbed inputs (e.g. the robustness test's noisy
copy) get their own cache entry, so caching never compromises correctness.
Disable it if you need a fresh prediction every call:
ctx = TestContext(model=model, X_val=X_val, y_val=y_val, cache_predictions=False)
YAML suites
Define your contract declaratively — no code needed:
suite:
name: "Credit Scoring Model"
tests:
- type: minimum_accuracy
params: {threshold: 0.85}
- type: group_performance
params: {metric: accuracy, threshold: 0.8, group_col: "gender"}
- type: robustness
params: {noise_std: 0.01, max_drop: 0.03}
- type: data_drift
params: {features: [age, income], max_psi: 0.15}
- type: equal_opportunity
params: {protected: "gender", max_diff: 0.1}
- type: statistical_parity
params: {protected: "gender", max_diff: 0.1, min_ratio: 0.8}
- type: data_invariant
params: {expected_columns: [age, income], max_null_ratio: 0.02}
Multi-framework support
TestContext talks to models through a small adapter interface
(modeltest.wrappers). Out of the box it normalizes:
- scikit-learn estimators (
predict, andpredict_probawhen available) - PyTorch
nn.Module(predict= argmax over logits,predict_proba= softmax) - Keras / TensorFlow models (binary threshold or multiclass argmax)
Pass any of these straight to suite.run(model, ...); the right adapter is
picked automatically. Custom framework? Implement a
ModelWrapper subclass and pass an instance as the model.
scikit-learn Pipelines (feature engineering + model) work out of the box:
the suite predicts through the whole pipeline, model_features filters
validation data to the pipeline's raw input columns, and the SHAP-based
explainability tests explain the final estimator against the engineered
features (e.g. num__age, cat__cat_a).
Built-in test types
type (YAML) |
Class | Checks |
|---|---|---|
minimum_accuracy |
MinimumAccuracyTest |
Global metric above threshold |
group_performance |
GroupPerformanceTest |
Metric above threshold per subgroup |
confidence_threshold |
ConfidenceThresholdTest |
Metric floor via bootstrap CI (lower/upper bound) |
robustness |
RobustnessTest |
Performance under feature noise |
data_invariant |
DataInvariantTest |
Expected columns / null ratios |
no_null |
NoNullTest |
No missing values |
data_drift |
DataDriftTest |
PSI between train & validation |
ks |
KSTest |
KS p-value per column |
equal_opportunity |
EqualOpportunityTest |
Balanced TPR across protected groups |
statistical_parity |
StatisticalParityTest |
Balanced selection rate (+ 4/5ths rule) |
feature_dominance |
FeatureDominanceTest |
No single feature dominates attribution |
top_features |
TopFeaturesTest |
Top-K attributed features are expected |
Explainability tests use SHAP. Install with
pip install modeltest[explain](ormodeltest'sexplainextra). You can also pass your own explainer callable to any explainability test.
Custom tests
Any class subclassing ModelTest can be referenced from a YAML suite
directly, by dotted import path — no registration required:
suite:
name: "Income Model"
tests:
- type: minimum_accuracy
params: {threshold: 0.85}
- type: myproject.custom_tests:ZeroPredictionShareTest
params: {min_positive_share: 0.01}
Both module.path:Class and module.path.Class work. The module is looked
up on the import path (your current working directory is added automatically).
Programmatic registration is also available for short, friendlier names:
from modeltest import register
register("zero_share", ZeroPredictionShareTest)
# ...now use `type: zero_share` in YAML
MLflow integration
Log a finished validation run into MLflow as an experiment run — one param per test, one metric per numeric value, and the full JSON report saved as an artifact.
pip install modeltest[mlflow]
import mlflow
from modeltest import ModelSuite
from modeltest.integrations.mlflow import log_suite_result
from modeltest.scenarios import MinimumAccuracyTest
suite = ModelSuite(name="fraud-v2")
suite.add_test(MinimumAccuracyTest(threshold=0.85))
result = suite.run(model, X_val, y_val)
with mlflow.start_run():
log_suite_result(result)
log_suite_result also supports a run_id parameter for logging into a
specific (possibly already-finished) run, plus optional param_prefix /
metric_prefix to namespace the logged names.
Development
Install dev tools and run the quality gates:
make install # pip install -e ".[dev]"
make lint # ruff check
make format # ruff format
make test # pytest (with 80% coverage gate)
make precommit # install git pre-commit hooks (lint+format)
Continuous integration mirrors these gates: lint, dist-check (build +
twine check), test-library (with coverage) and validate-model all run
on every push / PR
(.github/workflows/validate.yml).
CI/CD
A ready-to-use GitHub Actions workflow runs the library's own tests and validates your model contract on every push / PR. It publishes both reports (as JUnit) and fails the pipeline if the model doesn't meet its contract.
The model job trains a sample model and validates it:
modeltest validate --suite examples/suite.yaml --model examples/model.pkl \
--data examples/validation.csv --target target --train-data examples/train.csv
To point it at your real artifacts, update the validate-model job's run step paths.
Metadata
Release files for modeltest 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| modeltest-0.2.1.tar.gz | 42.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| modeltest-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 71.7 kB
Release files / modeltest-0.2.1.tar.gz
| Download URL | modeltest-0.2.1.tar.gz |
|---|---|
| Size | 42.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d328c61a8e3bef80c8ea9b2bcd8b001322dad6bc0c508c887f93d61d493a3fdb
|
|
BLAKE2b-256 checksum How to use checksums |
e084173dd88bd109f731e73850b4a691ff5b60f69f12d9dbf93fad04a62b88e1
|
| 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 Sep 21, 2026.
Transparency logRelease files / modeltest-0.2.1-py3-none-any.whl
| Download URL | modeltest-0.2.1-py3-none-any.whl |
|---|---|
| Size | 29.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e08fc470c77650b375db93d55ea284a97c6f8d25c64688e910240fb84023c5a7
|
|
BLAKE2b-256 checksum How to use checksums |
4178d8ce6b6bfba331e99deeba66f0a407dc108aee1f5dab4302e113fe67555a
|
| 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 Sep 21, 2026.
Transparency log