Skip to content

Improve deprecation message for class-scoped fixtures as instance methods#14764

Open
The-Compiler wants to merge 3 commits into
pytest-dev:mainfrom
The-Compiler:improve-classmethod-fixture-message
Open

Improve deprecation message for class-scoped fixtures as instance methods#14764
The-Compiler wants to merge 3 commits into
pytest-dev:mainfrom
The-Compiler:improve-classmethod-fixture-message

Conversation

@The-Compiler

@The-Compiler The-Compiler commented Jul 22, 2026

Copy link
Copy Markdown
Member

Shorten the message with -Werror slightly via __tracebackhide__, point out the name of the fixture, and fix some grammar issues.

Before:

Details

======================================================= ERRORS =======================================================
______________________________________ ERROR at setup of TestFixture.test_fixt _______________________________________

cls = <class '_pytest.runner.CallInfo'>, func = <function call_and_report.<locals>.<lambda> at 0x7f69abbe7cc0>
when = 'setup', reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
    ) -> CallInfo[TResult]:
        """Call func, wrapping the result in a CallInfo.
    
        :param func:
            The function to call. Called without arguments.
        :type func: Callable[[], _pytest.runner.TResult]
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        instant = timing.Instant()
        try:
>           result: TResult | None = func()
                                     ^^^^^^

src/_pytest/runner.py:361: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/_pytest/runner.py:250: in <lambda>
    lambda: runtest_hook(item=item, **kwds),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/logging.py:858: in pytest_runtest_setup
    yield
src/_pytest/capture.py:895: in pytest_runtest_setup
    return (yield)
            ^^^^^
src/_pytest/runner.py:170: in pytest_runtest_setup
    item.session._setupstate.setup(item)
src/_pytest/runner.py:536: in setup
    col.setup()
src/_pytest/python.py:1712: in setup
    self._request._fillfixtures()
src/_pytest/fixtures.py:807: in _fillfixtures
    item.funcargs[argname] = self.getfixturevalue(argname)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/fixtures.py:631: in getfixturevalue
    fixturedef = self._get_active_fixturedef(argname)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/fixtures.py:727: in _get_active_fixturedef
    fixturedef.execute(request=subrequest)
src/_pytest/fixtures.py:1233: in execute
    result: FixtureValue = ihook.pytest_fixture_setup(
.venv/lib/python3.14/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/setuponly.py:36: in pytest_fixture_setup
    return (yield)
            ^^^^^
src/_pytest/fixtures.py:1314: in pytest_fixture_setup
    fixturefunc = resolve_fixture_function(fixturedef, request)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

fixturedef = <FixtureDef argname='fixt' scope='class' baseid='test_fixt.py::TestFixture'>
request = <SubRequest 'fixt' for <Function test_fixt>>

    def resolve_fixture_function(
        fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
    ) -> _FixtureFunc[FixtureValue]:
        """Get the actual callable that can be called to obtain the fixture
        value."""
        fixturefunc = fixturedef.func
        # The fixture function needs to be bound to the actual
        # request.instance so that code working with "fixturedef" behaves
        # as expected.
        instance = request.instance
    
        if fixturedef._scope is Scope.Class:
            # Check if fixture is an instance method (bound to instance, not class)
            if hasattr(fixturefunc, "__self__"):
                bound_to = fixturefunc.__self__
                # classmethod: bound_to is the class itself (a type)
                # instance method: bound_to is an instance (not a type)
                if not isinstance(bound_to, type):
>                   warnings.warn(CLASS_FIXTURE_INSTANCE_METHOD, stacklevel=2)
E                   pytest.PytestRemovedIn10Warning: Class-scoped fixture defined as instance method is deprecated.
E                   Instance attributes set in this fixture will NOT be visible to test methods,
E                   as each test gets a new instance while the fixture runs only once per class.
E                   Use @classmethod decorator and set attributes on cls instead.
E                   See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method

src/_pytest/fixtures.py:1290: PytestRemovedIn10Warning
============================================== short test summary info ===============================================
ERROR test_fixt.py::TestFixture::test_fixt - pytest.PytestRemovedIn10Warning: Class-scoped fixture defined as instance method is deprecated.

after:

Details
======================================================= ERRORS =======================================================
______________________________________ ERROR at setup of TestFixture.test_fixt _______________________________________

cls = <class '_pytest.runner.CallInfo'>, func = <function call_and_report.<locals>.<lambda> at 0x7fc8fd6235e0>
when = 'setup', reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
    ) -> CallInfo[TResult]:
        """Call func, wrapping the result in a CallInfo.
    
        :param func:
            The function to call. Called without arguments.
        :type func: Callable[[], _pytest.runner.TResult]
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        instant = timing.Instant()
        try:
>           result: TResult | None = func()
                                     ^^^^^^

src/_pytest/runner.py:361: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/_pytest/runner.py:250: in <lambda>
    lambda: runtest_hook(item=item, **kwds),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/logging.py:858: in pytest_runtest_setup
    yield
src/_pytest/capture.py:895: in pytest_runtest_setup
    return (yield)
            ^^^^^
src/_pytest/runner.py:170: in pytest_runtest_setup
    item.session._setupstate.setup(item)
src/_pytest/runner.py:536: in setup
    col.setup()
src/_pytest/python.py:1712: in setup
    self._request._fillfixtures()
src/_pytest/fixtures.py:808: in _fillfixtures
    item.funcargs[argname] = self.getfixturevalue(argname)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/fixtures.py:632: in getfixturevalue
    fixturedef = self._get_active_fixturedef(argname)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/fixtures.py:728: in _get_active_fixturedef
    fixturedef.execute(request=subrequest)
src/_pytest/fixtures.py:1234: in execute
    result: FixtureValue = ihook.pytest_fixture_setup(
.venv/lib/python3.14/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/setuponly.py:36: in pytest_fixture_setup
    return (yield)
            ^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

fixturedef = <FixtureDef argname='fixt' scope='class' baseid='test_fixt.py::TestFixture'>
request = <SubRequest 'fixt' for <Function test_fixt>>

    def pytest_fixture_setup(
        fixturedef: FixtureDef[FixtureValue], request: SubRequest
    ) -> FixtureValue:
        """Execution of fixture setup."""
        kwargs = {}
        for argname in fixturedef.argnames:
            kwargs[argname] = request.getfixturevalue(argname)
    
>       fixturefunc = resolve_fixture_function(fixturedef, request)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       pytest.PytestRemovedIn10Warning: Class-scoped fixtures defined as instance methods are deprecated.
E       Instance attributes set in the 'fixt' fixture will NOT be visible to test methods,
E       as each test gets a new instance while the fixture runs only once per class.
E       Use a @classmethod decorator below @pytest.fixture and set attributes on cls instead.
E       See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method

src/_pytest/fixtures.py:1323: PytestRemovedIn10Warning
============================================== short test summary info ===============================================
ERROR test_fixt.py::TestFixture::test_fixt - pytest.PytestRemovedIn10Warning: Class-scoped fixtures defined as instance methods are deprecated.
================================================== 1 error in 0.17s =================================================

Still definitely not optimal, but I can't think of a way to further improve this easily.

@nicoddemus

Copy link
Copy Markdown
Member

Still definitely not optimal, but I can't think of a way to further improve this easily.

Indeed.

Once we turn this into an error, we can then raise UsageError, which will show no traceback at all.

…hods

Shorten the message with -Werror slightly via __tracebackhide__,
point out the name of the fixture, and fix some grammar issues.
@The-Compiler
The-Compiler force-pushed the improve-classmethod-fixture-message branch from a6b036a to 320fc17 Compare July 22, 2026 15:14
@The-Compiler

The-Compiler commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

don't think this needs a changelog, opinions?

@nicoddemus

Copy link
Copy Markdown
Member

don't think this needs a changelog, opinions?

Agreed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants