pytest-depper: Run only the tests you need, not all the tests you have
Smart test selection based on AST-level code dependency analysis.
Diffs your current branch vs main, and then gets which tests hit those lines changed, and runs those.
The Problem
Imagine you have a 3,000-line Python file (you're a typical python dev) with 2,000 individual tests covering various parts of it. You make a small change—just 8 lines. Should you run all 2,000 tests? No.
Most test selection tools either:
- Run everything (slow)
- Use coverage data from previous runs (requires pre-computed data, breaks with new code)
- Rely on naming conventions (fragile, incomplete)
The Solution
pytest-depper analyzes your code's actual import dependencies at the AST level to determine exactly which tests are affected by your changes.
When you change those 8 lines, pytest-depper:
- Diffs your branch against main to find changed files
- Builds a complete dependency graph of your codebase using AST parsing
- Traces which tests import (directly or transitively) the changed code
- Runs only those ~20 affected tests instead of all 2,000
No pre-computation. No cached state. No guessing.
Key Advantages Over Similar Tools
vs pytest-testmon
| Feature | pytest-depper | pytest-testmon |
|---|---|---|
| Requires previous run data | ❌ No | ✅ Yes (.testmon file) |
| Works on fresh branches | ✅ Yes | ❌ No (needs to build cache first) |
| AST-level analysis | ✅ Yes | ✅ Yes |
| Git integration | ✅ Built-in | ❌ Manual |
| Dependency graph visualization | ✅ Yes (--depper-debug) |
⚠️ Limited |
Why this matters: pytest-testmon requires running your full test suite at least once on the main branch to build its cache. If you check out a fresh branch or clone a new repo, you have no cache. pytest-depper works immediately by analyzing your code structure.
vs pytest-picked
pytest-picked only looks at changed test files and changed source files—it doesn't analyze the dependency graph. If you change models/user.py, and services/auth.py imports it, and tests/test_api.py tests the auth service, pytest-picked won't detect this transitive relationship.
vs Manual Test Selection
Running tests manually is error-prone. You might:
- Forget which tests cover which code
- Miss transitive dependencies (A imports B, B imports C, you changed C)
- Over-select tests out of caution (wasting time)
Installation
pip install pytest-depper
Or with poetry:
poetry add --group dev pytest-depper
Quick Start
As a Pytest Plugin
Simply add the --depper flag to your pytest command:
# Run only tests affected by changes compared to main branch
pytest --depper
# Compare against a different branch
pytest --depper --depper-base-branch=develop
# Show detailed dependency analysis
pytest --depper --depper-debug
As a Standalone CLI
Analyze dependencies without running tests:
# Show which tests would run
pytest-depper
# Compare to a different branch
pytest-depper --base-branch develop
# Show detailed dependency information
pytest-depper --debug
How It Works
pytest-depper uses function-level change detection with static import analysis to determine which tests need to run.
Step 1: Detect Changed Functions/Classes
git diff -U0 origin/main...HEAD
Output shows changed line numbers:
+++ b/src/calculator.py
@@ -5,3 +5,4 @@
def add(a, b):
+ # Added validation
return a + b
pytest-depper maps changed lines to function/class names using AST:
Line 6 changed → Part of function 'add'
Changed symbols: {'add'}
Step 2: Build Symbol Import Map
pytest-depper parses all Python files to track which specific symbols each test imports:
# test_math.py
from calculator import add, subtract # Imports: {'add', 'subtract'}
# test_multiply.py
from calculator import multiply # Imports: {'multiply'}
# test_integration.py
import calculator # Imports: {'*'} (entire module)
This creates a precise map:
{
'test_math.py': {'calculator.py': {'add', 'subtract'}},
'test_multiply.py': {'calculator.py': {'multiply'}},
'test_integration.py': {'calculator.py': {'*'}}
}
Step 3: Match Changed Symbols to Test Imports
For each changed symbol, pytest-depper finds tests that import it:
'add' function changed in calculator.py
test_math.py imports 'add' → RUN ✅
test_multiply.py imports 'multiply' → SKIP ❌
test_integration.py imports '*' → RUN ✅ (imports entire module)
Result: Only tests that import the changed functions/classes will run.
Example: 3000-Line File Scenario
# models.py (3000 lines, 50 functions)
def validate_email(email): ... # Changed (lines 145-150)
def validate_phone(phone): ...
def validate_address(addr): ...
# ... 47 more functions
# test_email.py
from models import validate_email # Imports: {'validate_email'}
# test_phone.py
from models import validate_phone # Imports: {'validate_phone'}
# test_integration.py
from models import * # Imports: {'*'}
When only validate_email changes:
- ✅
test_email.pyruns (imports the changed function) - ✅
test_integration.pyruns (imports everything with*) - ❌
test_phone.pyskipped (doesn't import the changed function) - ❌ 45+ other test files skipped
This is true function-level precision - not just file-level dependency tracking.
Configuration
Pytest Plugin Options
--depper
Enable smart test selection (default: False)
--depper-base-branch=BRANCH
Base branch to compare against (default: main)
--depper-debug
Print detailed dependency analysis (default: False)
--depper-run-all-on-error
Run all tests if no changed files detected or analysis fails (default: False)
Programmatic Usage
from pathlib import Path
from pytest_depper import DependencyAnalyzer
# Initialize analyzer
analyzer = DependencyAnalyzer(
project_root=Path("."),
exclusion_patterns=["venv", ".venv", "build"],
test_patterns=["test_", "_test.py", "/tests/"]
)
# Get changed files (you provide this)
changed_files = ["src/models/user.py", "src/utils/validators.py"]
# Get affected tests
affected_tests = analyzer.get_affected_tests(changed_files)
print(f"Run these tests: {affected_tests}")
# Debug: Print dependency info
analyzer.print_dependency_info(changed_files)
Real-World Example
Before pytest-depper
$ pytest
======================== test session starts ========================
collected 2847 items
tests/test_auth.py::test_login PASSED [ 0%]
tests/test_auth.py::test_logout PASSED [ 0%]
tests/test_auth.py::test_register PASSED [ 0%]
... (2844 more tests) ...
===================== 2847 passed in 284.32s =======================
⏱️ 4 minutes 44 seconds
After pytest-depper
$ pytest --depper
Depper: Found 2 changed files
- src/models/user.py
- src/utils/validators.py
Depper: 2 test files affected by changes
Depper: Selected 23 tests, deselected 2824 tests
======================== test session starts ========================
collected 23 items
tests/test_auth.py::test_login PASSED [ 4%]
tests/test_auth.py::test_logout PASSED [ 8%]
tests/test_auth.py::test_register PASSED [12%]
tests/test_validators.py::test_email_validation PASSED [16%]
... (19 more tests) ...
===================== 23 passed in 3.71s =======================
⏱️ 3.71 seconds
76x faster for a typical small change.
CI/CD Integration
GitHub Actions
name: Tests
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Need full history for git diff
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest-depper
- name: Run affected tests
run: pytest --depper --depper-run-all-on-error
GitLab CI
test:
script:
- pip install -r requirements.txt
- pip install pytest-depper
- pytest --depper --depper-base-branch=$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
Advanced Usage
Custom Test Patterns
analyzer = DependencyAnalyzer(
test_patterns=[
"test_", # files starting with test_
"_test.py", # files ending with _test.py
"/tests/", # files in tests/ directories
"/integration/", # custom: integration test directory
"spec.py", # custom: spec files
]
)
Custom Exclusion Patterns
analyzer = DependencyAnalyzer(
exclusion_patterns=[
"venv",
".venv",
"node_modules",
"build",
"dist",
".git",
"__pycache__",
"migrations", # custom: ignore migration files
"scripts", # custom: ignore utility scripts
]
)
Debug Output
$ pytest --depper --depper-debug
=== Dependency Analysis ===
Changed file: src/models/user.py
Dependencies: {'src/models/base.py', 'src/utils/validators.py'}
Files that depend on this: {'src/services/auth.py', 'src/services/user_service.py', 'tests/test_auth.py', 'tests/test_user.py'}
Direct test coverage: {'tests/test_user.py'}
Changed file: src/utils/validators.py
Dependencies: set()
Files that depend on this: {'src/models/user.py', 'tests/test_validators.py'}
Direct test coverage: {'tests/test_validators.py'}
Depper: Selected 4 test files
✓ tests/test_auth.py
✓ tests/test_user.py
✓ tests/test_user_service.py
✓ tests/test_validators.py
Limitations
pytest-depper uses static import analysis, not runtime coverage. This means it bases decisions on what tests import, not what they actually execute.
What pytest-depper CAN'T detect:
-
Imports that aren't used
# test_multiply.py from calculator import add, multiply # Imports both def test_multiply(): assert multiply(2, 3) == 6 # Only uses multiply
If
addchanges, this test will run even though it doesn't useadd. This is intentional conservatism - better to run too many tests than too few. -
Dynamic imports
module = __import__("my_module") # Not detected importlib.import_module("my_module") # Not detected
-
Module-level imports (
import module)import calculator # Imports: {'*'} - everything def test_add(): calculator.add(2, 3) # Uses only add
When you do
import calculator, pytest-depper treats it as importing everything from that module. Any change to the module will trigger this test.Recommendation: Use
from module import specific_functionfor better precision. -
String-based references
class_name = "UserModel" cls = globals()[class_name] # Not detected
-
Data-driven changes
- Changing a JSON config file won't trigger tests that read it
- Modifying database migrations won't trigger related tests
- Updating
.envfiles won't trigger affected tests
-
Changes outside functions/classes
- Module-level constants
- Global variables
- Changes in blank lines or comments only
These won't be detected as "symbol changes" and will fall back to file-level matching.
Recommended approach:
- Use
--depper-run-all-on-errorin CI to be safe - Run full test suite on main/develop branches
- Use pytest-depper for PR/feature branches to speed up development
- Combine with coverage tools to ensure you have adequate test coverage
FAQ
Q: Does this replace pytest-cov or coverage.py?
A: No. pytest-depper selects which tests to run. Coverage tools measure how much of your code is tested. Use them together.
Q: What if I rename a file?
A: Git treats renames as delete + add. pytest-depper will see the new file as changed and run all tests that import it.
Q: Does it work with monorepos?
A: Yes. Set project_root to your package directory:
analyzer = DependencyAnalyzer(project_root=Path("packages/my-package"))
Q: What Python versions are supported?
A: Python 3.8+. The AST analysis uses sys.stdlib_module_names (Python 3.10+) but falls back gracefully.
Q: Can I use this with tox or nox?
A: Yes:
# tox.ini
[testenv]
deps = pytest-depper
commands = pytest --depper
Q: Why "depper"?
A: Dependency-based test runner, or "deeper" analysis. Plus it's short and memorable.
Contributing
Contributions welcome! Please see CONTRIBUTING.md for guidelines.
Development Setup
# Clone the repo
git clone https://github.com/ryx2/pytest-depper.git
cd pytest-depper
# Install dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run linters
ruff check .
mypy pytest_depper
License
MIT License. See LICENSE for details.
Credits
Inspired by:
- pytest-testmon - for the concept of dependency-based test selection
- pytest-picked - for git integration with pytest
Built with ❤️ for developers who value their time.
Star this repo if pytest-depper saves you time!
Report issues at https://github.com/ryx2/pytest-depper/issues
Metadata
Release files for pytest-depper 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytest_depper-0.2.0.tar.gz | 22.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_depper-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 40.9 kB
Release files / pytest_depper-0.2.0.tar.gz
| Download URL | pytest_depper-0.2.0.tar.gz |
|---|---|
| Size | 22.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
165d8c54fa9713cd0151d0c070c115a2e78a63266d78e6f63bc8cf28d73158f0
|
|
BLAKE2b-256 checksum How to use checksums |
1f06f8c67301d2f6d4d630a7a4474c303e5ab904460faef32347ae1468eb5c0c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Release files / pytest_depper-0.2.0-py3-none-any.whl
| Download URL | pytest_depper-0.2.0-py3-none-any.whl |
|---|---|
| Size | 18.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
aff659dc73e8ec1fb56723ccef40c1586f2a47fb729db2bf885654d87c3930b9
|
|
BLAKE2b-256 checksum How to use checksums |
f7d2c8d08e7adc022a23f32a8215b523f900e1eac2b73e1abdca5dbd336a0509
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|