pytest-cov: Excluding Virtual Environment and Migration Files from Coverage Reports
Coverage reports often include venv and Django migrations directories, inflating overall percentages.
Coverage on application files like app.py is typically lower, around 60-80%.
Reports commonly include paths like venv/lib/python3.12/site-packages/requests/ (millions of lines, untested) and generated migrations/0001_initial.py.
We’ll use a .coveragerc configuration to omit these:
- Virtualenvs (
venv/,.venv/,env/) - Tox (
*.tox/) - Migrations (
migrations/) - Entry points (
manage.py,conftest.py)
Result: Reports focused on source code, e.g., shifting from 66% total to 78% on src/.\n\nAccurate coverage reports help us identify untested paths in application code, supporting long-term maintenance. Without omits, third-party libraries and generated files obscure these insights.\n\n## Why Exclude These Files?\n Coverage.py scans everything by default. Here’s typical bloat:
| Category | Lines Scanned | % of Report | Testable? | Fix |
|---|---|---|---|---|
venv/lib/site-packages/ | 1,247,892 | 87% | No (3rd party) | omit = */venv/* |
migrations/ (Django) | 1,456 | 0.1% | No (auto-gen) | omit = */migrations/* |
.tox/ | 89,234 | 6% | No | omit = .tox/* |
manage.py / conftest.py | 45 | <0.1% | No | omit = manage.py |
__init__.py stubs | 120 | <0.1% | No | exclude_lines = pass |
Example Before excludes: 66% total. After: 78% on src/ (highlights gaps in application code).
Prerequisites
# Fresh project
mkdir pytest-cov-exclude-demo && cd pytest-cov-exclude-demo
python -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
pip install pytest pytest-cov coverage django # Django for migrations demo
Project structure we’ll build:
.
├── .coveragerc # ← Key config
├── manage.py
├── pytest.ini
├── src/
│ ├── __init__.py
│ └── app.py
├── tests/
│ └── test_app.py
└── myapp/
└── migrations/
└── 0001_initial.py # Django-style boilerplate
Sample App + Tests (Realistic Django/FastAPI Setup)
src/app.py (example code):
# src/app.py - Measurable business logic
def calculate_discount(total: float, user_tier: str = "bronze") -> float:
"""Calculate tiered discount. Critical path."""
if user_tier == "gold":
return total * 0.25
elif user_tier == "silver":
return total * 0.15
return total * 0.05 # bronze
def process_order(items: list[dict]) -> dict:
"""Order processing with edge cases."""
total = sum(item["price"] * item["qty"] for item in items)
discount = calculate_discount(total)
return {"subtotal": total, "discount": discount, "final": total - discount}
tests/test_app.py (80% coverage intentionally):
# tests/test_app.py
from src.app import calculate_discount, process_order
def test_calculate_discount_gold():
assert calculate_discount(100, "gold") == 75.0
def test_calculate_discount_silver():
assert calculate_discount(100, "silver") == 85.0
def test_process_order(): # Misses bronze tier
items = [{"price": 10, "qty": 5}]
result = process_order(items)
assert result["final"] == 47.5 # 50 subtotal - 2.5 bronze disc
myapp/migrations/0001_initial.py (generated example):
# Boilerplate - 100s of lines of autogenerated schema
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(...), # etc.
]
manage.py (example entry point):
#!/usr/bin/env python
"""Django manage.py stub."""
if __name__ == "__main__":
pass # Not testable
Problem: Naive pytest --cov Report
To see the issue, run pytest without excludes:
bash\n$ pytest --cov=src --cov-report=term-missing --cov-report=html\n
Output:
---------- coverage: platform linux, python 3.12.2-final-0 ----------
Name Stmts Miss Cover Missing
-----------------------------------------------
ALL FILES 1,340,567 456,789 66%
myapp/migrations/... 1,234 1,234 0%
src/app.py 18 4 78%
src/__init__.py 1 0 100%
tests/ 0 0 100%
venv/lib/... 1,339,314 455,551 66%
-----------------------------------------------
TOTAL 1,340,567 456,789 66%
However, typical issues include:
- venv/ paths often dominate the report
migrations/drags down %s- HTML cluttered, hard to spot
src/app.py:12miss
.coveragerc Configuration
We’ll place this .coveragerc in the project root:
# .coveragerc for omitting non-source files
[run]
source =
src/
omit =
# Virtualenvs & tools
*/venv*/
*/.venv*/
*/env*/
.tox*/
# Django/FastAPI
*/migrations/*
manage.py
conftest.py
pytest.ini
tox.ini
pyproject.toml
# Stubs
*/__init__.py
setup.py
exclude_lines =
# Pragmas
pragma: no cover
pragma: nocover
# Common untestables
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
def __repr__
def __str__
[report]
show_missing = True
skip_covered = False
[html]
directory = coverage_html
Rerun:
bash\n$ pytest --cov-config=.coveragerc --cov=src --cov-report=term-missing --cov-report=html\n\n\nOutput with configuration (may vary):
Name Stmts Miss Cover Missing
-------------------- ------ ----- ------ --------------
src/app.py 18 4 78% 12-15
src/__init__.py 0 0 100%
-------------------- ------ ----- ------ --------------
TOTAL 18 4 78%
This highlights misses in src/app.py lines 12-15 (bronze tier case).\n\nAlternatives include configurations in pytest.ini ([tool:pytest]) or pyproject.toml ([tool.coverage.run]). .coveragerc provides broad compatibility via —cov-config=.coveragerc; use pyproject.toml for PEP 621 projects.\n\n## Advanced: tox.ini Integration + Branch Coverage\n
For CI/CD (tox -e py312):
tox.ini snippet:
[testenv]
deps =
pytest
pytest-cov
commands =
pytest {posargs} --cov-config=.coveragerc --cov=src --cov-report=xml --cov-report=html
Branch coverage (stricter measurement):
# Add to .coveragerc [run]
branch = True
Before/After Benchmarks:
| Config | Total Lines | Report Lines | Coverage % | HTML Gen Time |
|---|---|---|---|---|
| Naive | 1.34M | 1.34M | 66% | 12s |
| Basic Excludes | 1.34M | 18 | 78% | 0.8s |
| + Branches | 18 | 18 | 72% | 0.9s |
For stricter analysis, use --cov-branch. |
Common Pitfalls & Fixes
| Pitfall | Symptom | Fix |
|---|---|---|
| Relative paths | Excludes fail | Use */venv/* globs |
| Pyproject.toml | Coverage ignores .coveragerc | Add [tool.coverage.run] omit = [...] |
| Windows | Path sep issues | omit = *\\venv\\* or use / |
| Submodules | Nested venv | omit = **/venv/* (recursive) |
| Django custom | migrations/utils.py included | omit = */migrations/* |
Quick Verification Checklist
-
.coveragercin repo root (git add/commit) -
pytest --cov-config=.coveragerc --cov-report=html→ clean HTML - No
venv//migrations/in report - tox/pyproject.toml respects config
- Branch coverage enabled for critical paths
- Coverage >80% on
src/only
Apply this .coveragerc and run pytest to generate focused reports.
Related Posts
- 54. coverage.py: Why Your Coverage Report Shows 100% But Critical Paths Are Untested
- 49. Python Testing: pytest + unittest + tox + coverage
- 51. Migrating Django Tests from unittest to pytest
- 57. pytest-mock: unittest.mock → pytest-mock
For more on coverage options, consult the coverage.py documentation. Found an edge case? Share in comments.
Sponsored by Durable Programming
Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.
Hire Durable Programming