The go-to resource for upgrading Python, Django, Flask, and your dependencies.

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:

CategoryLines Scanned% of ReportTestable?Fix
venv/lib/site-packages/1,247,89287%No (3rd party)omit = */venv/*
migrations/ (Django)1,4560.1%No (auto-gen)omit = */migrations/*
.tox/89,2346%Noomit = .tox/*
manage.py / conftest.py45<0.1%Noomit = manage.py
__init__.py stubs120<0.1%Noexclude_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:12 miss

.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:

ConfigTotal LinesReport LinesCoverage %HTML Gen Time
Naive1.34M1.34M66%12s
Basic Excludes1.34M1878%0.8s
+ Branches181872%0.9s
For stricter analysis, use --cov-branch.

Common Pitfalls & Fixes

PitfallSymptomFix
Relative pathsExcludes failUse */venv/* globs
Pyproject.tomlCoverage ignores .coveragercAdd [tool.coverage.run] omit = [...]
WindowsPath sep issuesomit = *\\venv\\* or use /
SubmodulesNested venvomit = **/venv/* (recursive)
Django custommigrations/utils.py includedomit = */migrations/*

Quick Verification Checklist

  • .coveragerc in 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.

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