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

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.Package no longer Module
  • Files/dirs alphabetical
  • pytest_collect_directory hook
  • 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

StepCommand
Repropytest --collect-only -v
Auditpytest -r w --collect-only
Fix abstractsAdd skipif
Testpytest

Benchmarks (500 classes)

BeforeWarningsCollect Time
Legacy01.2s
8.0 fixed01.1s

Negligible.

Pitfalls

  • --disable-warnings: Hides, doesn’t fix.
  • Plugins: Update pytest-xdist, etc.
  • Legacy nose/unittest: Migrate.

<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