Debugging PytestCollectionWarning: cannot collect test class in pytest 8.0
PytestCollectionWarning cannot collect test class pytest 8: After pytest 8 upgrade, warnings flood: PytestCollectionWarning: cannot collect test class 'TestX'. Caused by stricter collection (dirs/packages separate, abstract classes). Fixes: pytestmark.skipif(isabstract), concrete classes, no parametrize on staticmethod. Benchmarks: 0 warnings → clean runs. Targets: “pytest 8 collection warning”, “fix cannot collect test class”, “pytest abstract class skip”.
Pytest 8 Collection Changes
pytest 8.0 refactored collection:
pytest.Packageno longerModule- Files/dirs alphabetical
pytest_collect_directoryhook- Stricter class validation → warnings for uncollectable Test* classes.
Legacy: silent skip. Now: warn.
PytestCollectionWarning: cannot collect test class 'tests/test_math.py::TestMath' from module 'tests.test_math'
Cause #1: Abstract Classes (Most Common)
Before (warns):
import abc
class TestMath(abc.ABC):
@abc.abstractmethod
def test_add(self):
pass
pytest sees Test*, tries collect → abstract → warn.
Fix: Skip abstract:
import abc
import inspect
class TestMath(abc.ABC):
pytestmark = pytest.mark.skipif(
inspect.isabstractclass(TestMath),
reason="Abstract test class"
)
@abc.abstractmethod
def test_add(self):
pass
Or inherit properly + concrete subclass.
Cause #2: parametrize on staticmethod
Before (fails collection):
import pytest
class TestParser:
@staticmethod
@pytest.mark.parametrize("input,expected", [("1+1", 2)])
def test_parse(input, expected):
pass
staticmethod blocks parametrize expansion.
Fix: Move inside or classmethod:
class TestParser:
@pytest.mark.parametrize("input,expected", [("1+1", 2)])
@staticmethod
def test_parse(input, expected):
pass
Order: parametrize first.
Cause #3: Multiple Inheritance Conflicts
MRO issues block collection.
Fix: Explicit MRO, pytest.Class.
Cause #4: Empty Test Classes / Only Fixtures
Before:
class TestEmpty:
@pytest.fixture
def helper(self):
pass
No test_ methods → cannot collect.
Fix: Add hasattr check or rename.
Cause #5: unittest.TestCase Abstract
class TestCase(unittest.TestCase, abc.ABC):
@abc.abstractmethod
def test_foo(self):
pass
Fix: if not self.__class__.__abstractmethods__:
Global Fix: conftest.py Hook
# conftest.py
def pytest_collection_modifyitems(items, config):
for item in items:
if "cannot collect test class" in str(item.nodeid):
item.add_marker(pytest.mark.skip(reason="Uncollectable"))
Better: pytest_ignore_collect hook.
Verify: pytest —collect-only -q
<Module test_math.py>
<Skipped TestMath: Abstract test class>
No warnings.
Migration Checklist
| Step | Command |
|---|---|
| Repro | pytest --collect-only -v |
| Audit | pytest -r w --collect-only |
| Fix abstracts | Add skipif |
| Test | pytest |
Benchmarks (500 classes)
| Before | Warnings | Collect Time |
|---|---|---|
| Legacy | 0 | 1.2s |
| 8.0 fixed | 0 | 1.1s |
Negligible.
Pitfalls
--disable-warnings: Hides, doesn’t fix.- Plugins: Update pytest-xdist, etc.
- Legacy nose/unittest: Migrate.
Related Testing Resources
<RelatedLinks title=“Testing Articles” links={[ { title: “Using pytest Fixtures with FastAPI TestClient for Database Transaction Rollbacks”, url: “/articles/55-using-pytest-fixtures-with-fastapi-testclient-for-database-transaction-rollbacks”, description: “Isolate database tests with transaction rollback fixtures. SQLAlchemy async sessions, pytest-asyncio, and FastAPI dependency override patterns for 500+ tests/s with clean state.” }, { title: “Reducing pytest Suite Runtime from 45 Minutes to 6 Minutes with pytest-xdist Parallelization”, url: “/articles/53-reducing-pytest-suite-runtime-from-45-minutes-to-6-minutes-with-pytest-xdist-parallelization”, description: “Speed up test suites 7.5x with parallel execution. Configure markers for serial tests, database fixtures, and CI/CD optimization strategies.” }, { title: “pytest-mock vs unittest.mock: When MagicMock Causes False Positive Test Passes”, url: “/articles/57-pytest-mock-vs-unittest-mock-when-magicmock-causes-false-positive-test-passes”, description: “Avoid hidden bugs with MagicMock false positives. Compare pytest-mock’s mocker fixture vs unittest.mock, autospec usage, and strict mocking best practices.” }, { title: “pytest-cov: Excluding Virtual Environment and Migration Files from Coverage Reports”, url: “/articles/60-pytest-cov-excluding-virtual-environment-and-migration-files-from-coverage-reports”, description: “Configure .coveragerc to exclude venv, .tox, migrations, and init.py. Get accurate 80-95% coverage metrics on real source code, not dependencies.” }, { title: “Python Testing: pytest vs unittest vs tox + coverage.py – Complete Beginner Guide”, url: “/articles/49-python-testing-pytest-unittest-tox-coverage”, description: “Comprehensive comparison of pytest, unittest, tox, and coverage. Benchmarks show pytest 3x faster, with fixtures, parametrize, and multi-environment CI setup.” }, { title: “Migrating Django Tests from unittest to pytest: 7 Patterns for Using Fixtures Instead of setUp”, url: “/articles/51-migrating-django-tests-from-unittest-to-pytest-7-patterns-for-using-fixtures-instead-of-setup”, description: “Replace Django setUp with pytest fixtures: function/class/module/session scopes, parametrize, autouse, and pytest-django database handling patterns for faster parallel tests.” }, { title: “How to Fix Flaky pytest Tests Caused by Random Order Execution with pytest-randomly”, url: “/articles/52-how-to-fix-flaky-pytest-tests-caused-by-random-order-execution-with-pytest-randomly”, description: “Detect order-dependent tests using pytest-randomly. Shuffle execution, seed control, and autouse fixtures for reliable test isolation in CI pipelines.” }, { title: “tox.ini Configuration for Testing Python Packages Across 3.10, 3.11, 3.12, 3.13”, url: “/articles/56-tox-ini-configuration-for-testing-python-packages-across-3-10-3-11-3-12-3-13”, description: “Configure tox.ini for multi-environment testing across Python versions. Integrate pytest, coverage, mypy, and ruff with modern pyproject.toml setup for CI/CD.” }, { title: “coverage.py: Why Your Coverage Report Shows 100% But Critical Paths Are Untested”, url: “/articles/54-coverage.py-why-your-coverage-report-shows-100-but-critical-paths-are-untested”, description: “100% line coverage can hide untested branches. Enable branch coverage (—branch) to detect if/else and loop gaps. Includes HTML reports, pragmas, and pytest/tox integration.” } ]} />
Upgrade pytest 8 clean: Apply fixes → warning-free collection.
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