Usage Guide
Basic Usage
Activate the plugin by passing the --impacted flag along with --impacted-module to pytest:
This runs only the tests impacted by files with unstaged modifications in your current git repository.
Note
The --impacted-module value must be a valid Python package name (underscores, not hyphens).
If you accidentally use hyphens, the plugin will suggest the corrected name.
Git Modes
Unstaged Mode (default)
Compares your working directory changes (including untracked files) against the current HEAD:
Branch Mode
Compares all commits on your current branch against a base branch:
pytest --impacted \
--impacted-module=my_package \
--impacted-git-mode=branch \
--impacted-base-branch=main
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 project layout), use --impacted-tests-dir so the dependency graph includes them:
The tests directory does not need to contain __init__.py — the plugin uses filesystem-based discovery that matches pytest's own behavior.
Monorepo / src-Layout Support
The plugin works in monorepos where the Python project lives in a subdirectory — the .git directory does not need to be in the current working directory. Parent directories are searched automatically to find the git repository.
src-Layout Projects
For projects using the src-layout convention (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 (no __init__.py) and uses the correct importable module name (my_package) for dependency analysis. This means AST-parsed imports like from my_package import ... will correctly match discovered modules.
Tip
If you accidentally pass just --impacted-module=my_package in a src-layout project, the plugin will detect that src/my_package exists and suggest the correct flag.
Monorepo Layout
In a monorepo where the Python project is nested under a subdirectory:
monorepo/ ← git root
backend/ ← working directory (pyproject.toml here)
src/
my_package/
tests/
frontend/
Run pytest from the backend/ directory as usual. The plugin will:
- Find the git repository by searching parent directories
- Convert git-relative file paths (e.g.
backend/src/my_package/module.py) to working-directory-relative paths (e.g.src/my_package/module.py) - Only consider changes within the working directory — changes in sibling directories (e.g.
frontend/) are ignored
Impact Analysis Strategies
The plugin uses a modular, strategy-based architecture to determine which tests are affected by code changes. Strategies are composable — the default pipeline combines three built-in strategies.
ASTImpactStrategy
The core strategy. It uses static analysis to:
- Discover all submodules via filesystem scanning (no imports executed)
- Parse each source file's AST to extract import relationships
- Build a dependency graph with NetworkX
- Trace transitive dependencies from changed modules to test modules
PytestImpactStrategy
Extends the AST analysis with pytest-specific dependency detection:
conftest.pyhandling: When aconftest.pyfile is modified, all tests in the same directory and subdirectories are considered impacted. This is critical becauseconftest.pyfiles are implicitly loaded by pytest at runtime and are not visible through normal import analysis.- Designed to be extended with additional pytest-specific heuristics in the future.
DependencyFileImpactStrategy
Detects changes in dependency and configuration files. When these files change, any test could potentially be affected — so all test modules are marked as impacted.
Monitored files include:
uv.lock,requirements.txt,pyproject.tomlPipfile,Pipfile.lock,poetry.locksetup.py,setup.cfgrequirements/*.txt(nested requirements files)
This strategy is enabled by default. To disable it, use:
Tip
This is especially useful in CI where dependency version bumps (e.g. updating uv.lock) don't change any .py files but could still break tests due to changed third-party behavior.
CompositeImpactStrategy
Combines multiple strategies, deduplicating and sorting results. The default composition is:
CompositeImpactStrategy([
ASTImpactStrategy(),
PytestImpactStrategy(),
DependencyFileImpactStrategy(),
])
Custom Strategies
You can implement your own strategy by subclassing ImpactStrategy and passing it to the get_impacted_tests() API:
from pathlib import Path
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(),
)
CI Integration
For CI pipelines where git analysis and test execution happen in separate stages, use the standalone impacted-tests CLI:
# 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 = false # set to true to disable dep file detection
CLI flags override these defaults.
Input Validation
The plugin validates configuration early and provides helpful error messages:
| Scenario | What happens |
|---|---|
--impacted-module=my-package (hyphens) |
Suggests my_package if it exists |
--impacted-module=my_package (src-layout) |
Suggests src/my_package if found under src/ |
--impacted-module=nonexistent |
Clear error with instructions to check the package name and working directory |
--impacted-tests-dir=bad_path |
Error indicating the directory doesn't exist |
--impacted-base-branch=no_such_branch |
Error listing available git refs |
| No git repository found | Clear error indicating no .git found at or above the working directory |
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 |
How It Works (Pipeline)
graph LR
A[Git diff] --> B[Changed files]
B --> C[Module resolution]
C --> D[AST import parsing]
D --> E[Dependency graph]
E --> G[Impacted tests]
B --> F[Dep file detection]
F -->|uv.lock, requirements.txt, etc.| G
- 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: false positives (running a test that didn't need to run) are preferred over false negatives (missing a test that should have run).
Performance: Rust Acceleration
For large codebases with many modules, import parsing can become a bottleneck. An optional Rust extension provides 37-65x faster import parsing using ruff's Python parser and rayon for parallel file processing.
Installation
Install with the fast extra to get pre-built Rust wheels (no Rust toolchain needed):
Or with uv:
For development from source (requires a Rust toolchain and maturin):
How It Works
When the Rust extension (pytest_impacted_rs) is installed, build_dep_tree() automatically uses parallel batch parsing instead of sequential astroid parsing. No configuration or flags are needed — the extension is detected at import time.
The Rust extension:
- Reads all source files in parallel via rayon
- Parses Python ASTs using ruff's hand-written recursive descent parser (the same parser used by the ruff linter)
- Extracts import statements by recursively walking all statement bodies (including
if,try,with, function, and class blocks) - Returns results as Python
list[str]— only the final data crosses the Rust/Python boundary
Benchmarks
Run the included benchmark script to measure speedup on your codebase:
Note
The Rust extension is completely optional. When not installed, the pure-Python (astroid) implementation is used automatically. All functionality works identically in both modes.