pytest-impacted
Run only the tests that matter. A pytest plugin that uses git diff, AST parsing, and dependency graph analysis to selectively run tests impacted by your code changes.
pytest --impacted --impacted-module=my_package # unstaged changes
pytest --impacted --impacted-module=my_package \
--impacted-git-mode=branch \
--impacted-base-branch=main # branch changes vs main
Key Features
| Feature | Details | |
|---|---|---|
| Fast feedback | Only runs tests affected by your changes โ skip the rest | |
| Dependency-aware | Follows import chains transitively, not just direct file changes | |
| No imports at analysis time | Filesystem discovery + AST parsing โ no module-level side effects | |
| pytest-native | Works as a standard pytest plugin with familiar CLI options | |
| conftest.py aware | Changes to conftest.py automatically impact all tests in scope |
|
| Dependency-file aware | Changes to uv.lock, requirements.txt, pyproject.toml etc. trigger all tests |
|
| CI-friendly | Standalone impacted-tests CLI for two-stage CI pipelines |
|
| Rust-accelerated | Optional Rust extension for 37-65x faster import parsing on large codebases | |
| Helpful errors | Validates config early with clear messages and suggestions |
[!CAUTION] This project is currently in beta. Please report bugs via the Issues tab.
Installation
Or with uv:
For 37-65x faster import parsing on large codebases, install with the optional Rust extension:
Requires Python 3.11+.
Quick Start
1. Run tests impacted by uncommitted changes:
2. Run tests impacted by branch changes (vs main):
pytest --impacted \
--impacted-module=my_package \
--impacted-git-mode=branch \
--impacted-base-branch=main
3. Include tests outside the package directory:
That's it. Unaffected tests are automatically skipped.
How It Works
Git diff โ Changed files โ Module resolution โ AST import parsing โ Dependency graph โ Impacted tests
โ Dependency file detection โ All tests (if dep files changed)
- Git introspection identifies which files changed (unstaged edits or branch diff)
- Filesystem discovery maps file paths to Python module names โ without importing anything
- AST parsing (via astroid, or the optional Rust extension using ruff's parser) extracts import relationships from source files
- Dependency graph (via NetworkX) traces transitive dependencies from changed modules to test modules
- Dependency file detection โ if files like
uv.lock,requirements.txt, orpyproject.tomlchanged, all tests are marked as impacted regardless of import analysis - Test filtering skips tests whose modules are not in the impact set
The philosophy is to err on the side of caution: we favor false positives (running a test that didn't need to run) over false negatives (missing a test that should have run).
Strategy-Based Architecture
Impact analysis is pluggable via a strategy pattern. The default pipeline combines three strategies:
| Strategy | What it does |
|---|---|
| ASTImpactStrategy | Traces transitive import dependencies through the dependency graph |
| PytestImpactStrategy | Extends AST analysis with pytest-specific knowledge โ when a conftest.py file changes, all tests in its directory and subdirectories are marked as impacted |
| DependencyFileImpactStrategy | When dependency files change (uv.lock, requirements.txt, pyproject.toml, etc.), all tests are marked as impacted |
All strategies are combined via CompositeImpactStrategy, which deduplicates and merges their results. Dependency file detection is enabled by default and can be disabled with --no-impacted-dep-files.
You can also supply a custom strategy via the get_impacted_tests() API:
from pytest_impacted.api import get_impacted_tests
from pytest_impacted.strategies import ImpactStrategy
class MyCustomStrategy(ImpactStrategy):
def find_impacted_tests(self, changed_files, impacted_modules, ns_module, **kwargs):
# your logic here
...
impacted = get_impacted_tests(
impacted_git_mode="branch",
impacted_base_branch="main",
root_dir=Path("."),
ns_module="my_package",
strategy=MyCustomStrategy(),
)
Usage
Git Modes
| Mode | Flag | What it compares |
|---|---|---|
| unstaged (default) | --impacted-git-mode=unstaged |
Working directory changes + untracked files |
| branch | --impacted-git-mode=branch |
All commits on current branch vs base branch |
The --impacted-base-branch flag accepts any valid git ref, including expressions like HEAD~4.
External Tests Directory
When your tests live outside the namespace package (a common layout), use --impacted-tests-dir so the dependency graph includes them:
Monorepo / src-Layout Support
The plugin works in monorepos where the Python project is nested in a subdirectory (the .git directory doesn't need to be in the working directory โ parent directories are searched automatically).
For src-layout projects (e.g. src/my_package/), point --impacted-module at the full path including the src/ prefix:
# From the project directory (e.g. monorepo/backend/)
pytest --impacted \
--impacted-module=src/my_package \
--impacted-tests-dir=tests
The plugin automatically detects that src/ is not a Python package and uses the correct importable module name (my_package) for dependency analysis.
CI Integration
For CI pipelines where git access and test execution happen in separate stages, use the impacted-tests CLI to generate the test file list:
# Stage 1: identify impacted tests
impacted-tests --module=my_package --git-mode=branch --base-branch=main > impacted_tests.txt
# Stage 2: run only those tests
pytest $(cat impacted_tests.txt)
Configuration via pyproject.toml
All CLI options can be set as defaults in your pyproject.toml (or pytest.ini):
[tool.pytest.ini_options]
impacted = true
impacted_module = "my_package"
impacted_git_mode = "branch"
impacted_base_branch = "main"
impacted_tests_dir = "tests"
# no_impacted_dep_files = true # uncomment to disable dep file detection
CLI flags override these defaults.
All Options
| Option | Default | Description |
|---|---|---|
--impacted |
false |
Enable the plugin |
--impacted-module |
(required) | Top-level Python package to analyze |
--impacted-git-mode |
unstaged |
Git comparison mode: unstaged or branch |
--impacted-base-branch |
(required for branch mode) | Base branch/ref for branch-mode comparison |
--impacted-tests-dir |
None |
Directory containing tests outside the package |
--no-impacted-dep-files |
false |
Disable dependency file change detection |
Alternatives
| Project | Notes |
|---|---|
| pytest-testmon | Most popular option. Uses coverage-based granular change tracking. More precise but heavier; may conflict with other plugins. |
| pytest-picked | Runs tests from directly modified files only โ no transitive dependency analysis. |
| pytest-affected | Appears unmaintained, no source repository. |
Performance: Optional Rust Acceleration
For large codebases, install the optional Rust extension to accelerate import parsing by 37-65x:
This installs pytest-impacted-rs, a pre-built Rust extension using ruff's parser and rayon for parallel file processing. The extension is automatically detected at runtime โ no configuration needed. When unavailable, the pure-Python (astroid) implementation is used.
Development
This project uses uv for dependency management.
# Setup
uv sync --all-extras --dev
# Run tests
uv run python -m pytest
# Run tests with coverage
uv run python -m pytest --cov=pytest_impacted --cov-branch tests
# Lint + format + type check
pre-commit run --all-files
# Install with Rust acceleration (pre-built wheels, no Rust toolchain needed)
pip install pytest-impacted[fast]
# Or build from source (requires Rust toolchain)
pip install maturin
cd rust && maturin develop --release
# Run parsing benchmarks
python -m benchmarks.bench_parsing