SQLAlchemy This Week: pytest Class Fixture Migration for pytest 10

   |   4 minute read   |   Using 771 words

SQLAlchemy shipped a small but important test harness update this week: class scoped pytest fixtures across the project now use @classmethod and cls instead of instance methods with self. The change tracks pytest 9.1 deprecation guidance and clears the path before pytest 10 drops the old pattern entirely.

Why pytest forced the change

pytest 9.1 started warning on class scoped fixtures defined as ordinary instance methods. The first parameter was historically self, which pytest treated as a test class instance even though the fixture never behaved like one. The project documented the breakage in changelog entry 13392 and closed ticket #13392 with a single commit on June 22.

For library users who only install SQLAlchemy from PyPI, this is invisible. For anyone who runs the upstream test suite, patches ORM behavior, or copies SQLAlchemy fixture patterns into their own database tests, the timing matters. pytest 10 will remove support for the self form. CI jobs that pin pytest but ignore warnings will start failing once that release lands.

The fix is mechanical but easy to miss in a large tree: decorate the fixture with @classmethod and rename the first argument to cls. SQLAlchemy applied that pattern consistently rather than waiting for a hard failure.

Base fixtures in lib/sqlalchemy/testing/fixtures/

Most SQLAlchemy integration tests inherit from shared fixture classes. Two of them drive table and mapper setup at class scope.

TablesTest in sql.py runs _setup_tables_test_class as an autouse class fixture. It calls _init_class, builds metadata once per class, runs inserts, yields control to tests, then tears down the metadata bind. MappedTest in orm.py extends that flow with ORM class and mapper setup on the same autouse hook.

Before this commit, both fixtures grabbed the class through cls = self.__class__. That indirection was a workaround for pytest accepting self on class scoped hooks. The new code drops the workaround:

@config.fixture(autouse=True, scope="class")
@classmethod
def _setup_tables_test_class(cls):
    cls._init_class()
    cls._setup_once_tables()
    ...

The mypy fixture module got the same treatment. MypyTest.cachedir and its helper _cachedir are now class methods. That keeps mypy stub checks sharing one cache directory per test class without tripping the pytest 9.1 warning path.

If you maintain a fork or vendor these fixtures, diff those three files first. They are the highest traffic entry points for SQL and ORM tests.

Plugin wrapper changes in pytestplugin.py

SQLAlchemy does not call pytest.fixture directly in most places. It routes through config.fixture, implemented in pytestplugin.py. That layer adds async wrappers and other test config hooks before pytest sees the function.

Class methods broke that pipeline. A @classmethod object is not a plain function, so the wrapper had to unwrap fn.__func__, run existing decorators, then rewrap with classmethod(fn) before applying the pytest fixture marker. The new logic sits inside the wrap helper around line 864:

def wrap(fn):
    is_classmethod = isinstance(fn, classmethod)
    if is_classmethod:
        fn = fn.__func__
    if config.any_async:
        fn = asyncio._maybe_async_wrapper(fn)
    if is_classmethod:
        fn = classmethod(fn)
    fn = fixture(fn)
    return fn

Without this step, converting fixtures to @classmethod would silently produce broken markers or drop async compatibility. Contributors adding new class scoped fixtures through testing.fixture or config.fixture should follow the same shape: class method first, let the plugin rewrap.

Direct test updates outside the shared bases

Not every class scoped fixture lives in the shared modules. The commit also touched tests that declared their own hooks.

CacheKeyTest.mapping_fixture in test/aaa_profiling/test_misc.py sets up profiling fixtures at class scope. _MutableNoHashFixture.set_class in test/ext/test_mutable.py swaps a global Foo model before mutable extension tests run. Both were instance methods with scope="class". Both are class methods now.

That matters because these files are where contributors copy patterns when adding new suites. Old examples using def fixture(self): under a class scoped decorator will fail on pytest 10 even if the shared bases are already fixed.

What to watch

Three practical notes for anyone running or extending this tree:

  • Pin pytest consciously. Warnings on pytest 9.1 mean you are on borrowed time. Plan for pytest 10 before it becomes the default in your distro or tox matrix.
  • Match the @classmethod pattern for new class scoped fixtures. Use cls, route through testing.fixture or config.fixture, and avoid reintroducing self.__class__ shortcuts in test classes.
  • Expect more churn in unreleased 2.0 changelog files. This fix landed in doc/build/changelog/unreleased_20/13392.rst, so it will ship with the next SQLAlchemy 2.0.x release rather than as a standalone announcement.

Activity in the lookback window was limited to this one commit: seven files, twenty eight insertions, nine deletions. No ORM API changes, no dialect updates, no runtime behavior shifts. The value is entirely in keeping the test harness compatible with pytest’s next major boundary. For a project the size of SQLAlchemy, that is the right kind of boring maintenance.



denis256 at denis256.dev