Pytest-cov is the easiest way to get detailed code coverage reports for your Python projects.
Let’s see it in action. Imagine you have a simple Python module:
# my_module.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
And a basic pytest test file:
# test_my_module.py
from my_module import add, subtract, multiply, divide
def test_add():
assert add(2, 3) == 5
def test_subtract():
assert subtract(5, 2) == 3
def test_multiply():
assert multiply(4, 5) == 20
def test_divide():
assert divide(10, 2) == 5
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(10, 0)
To measure coverage, you first need to install pytest-cov:
pip install pytest-cov
Then, run pytest with the --cov flag, specifying the module you want to cover:
pytest --cov=my_module
This will run your tests and generate a basic coverage report in your terminal:
============================= test session starts ==============================
platform linux -- Python 3.10.12, pytest-7.4.3, pluggy-1.3.0
rootdir: /path/to/your/project
plugins: cov-4.1.0
collected 5 items
test_my_module.py ..... [100%]
---------- coverage: platform linux, python 3.10.12-final-0 -----------
Name Stmts Miss Cover
---------------------------------
my_module.py 9 0 100%
---------------------------------
TOTAL 9 0 100%
============================== 5 passed in 0.10s ===============================
This terminal output tells you how many statements (Stmts) were executed, how many were missed (Miss), and the overall coverage percentage (Cover). In this case, 100% of my_module.py was covered by the tests.
For a more detailed look, you can generate an HTML report:
pytest --cov=my_module --cov-report=html
This will create a htmlcov/ directory in your project. Open htmlcov/index.html in your browser to explore an interactive report. You can click on my_module.py to see exactly which lines were executed and which were missed.
The core problem pytest-cov solves is making it easy to verify that your test suite is exercising the code it’s supposed to be testing. Without coverage, you might have tests that pass but don’t actually exercise critical logic or edge cases. pytest-cov provides the visibility to identify these gaps.
Under the hood, pytest-cov integrates with Python’s built-in coverage.py library. When you run pytest --cov=my_module, coverage.py is activated. It instruments your code by injecting small pieces of code that record when each line is executed. Pytest then runs your tests. After the tests complete, coverage.py analyzes the recorded execution data and generates the reports.
You can control which files or directories coverage.py tracks using the --cov argument. For example, pytest --cov=src would track all files within the src directory. You can also exclude specific files or directories using the --cov-exclude flag, like --cov-exclude="*/migrations/*".
One of the most powerful features is configuring pytest-cov via a pyproject.toml or .coveragerc file. This allows you to set defaults and avoid long command-line arguments. For instance, in pyproject.toml:
[tool.pytest.ini_options]
cov_source = ["src"]
cov_report = ["html", "term"]
cov_exclude = ["*/migrations/*", "*/tests/*"]
This configuration would automatically use src as the coverage source, generate both HTML and terminal reports, and exclude files in migrations and tests directories.
The one thing most people don’t know is how coverage.py handles branches. It doesn’t just count lines; it also tracks conditional statements like if, while, and for. When you see "branch coverage" in a report, it means coverage.py is tracking whether both the true and false outcomes of a condition were executed. For example, in my_module.py, the if b == 0: line has two possible branches: one where b is 0 and one where it’s not. A test that hits the ValueError covers one branch, and a test that performs a valid division covers the other. Missing branch coverage is a strong indicator of untested logic paths.
If you’re running your tests in a CI/CD pipeline, you’ll often want to fail the build if coverage drops below a certain threshold. You can achieve this with the --cov-fail-under option:
pytest --cov=my_module --cov-fail-under=90
This command will cause pytest to exit with a non-zero status code if the coverage for my_module.py is less than 90%.
The next step after ensuring good code coverage is often to analyze the quality of your tests, not just their quantity, using tools like pytest-flaky to identify tests that pass and fail intermittently.