Skip to content

pluggy requires from __future__ import annotations although it shouldn't in Python 3.14 #629

Closed

Description

@bersbersbers

This file is valid in Python 3.14, meaning, python conftest.py does not error:

conftest.py:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from _pytest.config import Config

def pytest_configure(config: Config) -> None: ...

In Python 3.13, it requires from __future__ import annotations.

Now, running pytest with the above conftest.py raises the following error. For some reason, pluggy seems to want to read the annotations and fails because Config is not available in non-type-checking mode.

Potential workarounds include:

  • Config -> "Config" (but ruff undoes that)
  • remove if TYPE_CHECKING (but ruff undoes that)
  • add from __future__ import annotations

All of those work, but none of those should be necessary.

(.3.14) C:\Git\Temp>pytest
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Git\Temp\.3.14\Scripts\pytest.exe\__main__.py", line 10, in <module>
    sys.exit(console_main())
             ~~~~~~~~~~~~^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 221, in console_main
    code = main()
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 191, in main
    config = _prepareconfig(new_args, plugins)
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 358, in _prepareconfig
    config: Config = pluginmanager.hook.pytest_cmdline_parse(
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        pluginmanager=pluginmanager, args=args
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_callers.py", line 167, in _multicall
    raise exception
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\helpconfig.py", line 124, in pytest_cmdline_parse
    config = yield
             ^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 1155, in pytest_cmdline_parse
    self.parse(args)
    ~~~~~~~~~~^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 1525, in parse
    self.hook.pytest_load_initial_conftests(
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        early_config=self, args=args, parser=self._parser
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_callers.py", line 167, in _multicall
    raise exception
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\warnings.py", line 128, in pytest_load_initial_conftests
    return (yield)
            ^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\capture.py", line 173, in pytest_load_initial_conftests
    yield
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 1239, in pytest_load_initial_conftests
    self.pluginmanager._set_initial_conftests(
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        args=args,
        ^^^^^^^^^^
    ...<8 lines>...
        ),
        ^^
    )
    ^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 599, in _set_initial_conftests
    self._try_load_conftest(
    ~~~~~~~~~~~~~~~~~~~~~~~^
        anchor,
        ^^^^^^^
    ...<2 lines>...
        consider_namespace_packages=consider_namespace_packages,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 637, in _try_load_conftest
    self._loadconftestmodules(
    ~~~~~~~~~~~~~~~~~~~~~~~~~^
        anchor,
        ^^^^^^^
    ...<2 lines>...
        consider_namespace_packages=consider_namespace_packages,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 677, in _loadconftestmodules
    mod = self._importconftest(
        conftestpath,
    ...<2 lines>...
        consider_namespace_packages=consider_namespace_packages,
    )
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 753, in _importconftest
    self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 834, in consider_conftest
    self.register(conftestmodule, name=registration_name)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\_pytest\config\__init__.py", line 519, in register
    plugin_name = super().register(plugin, name)
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_manager.py", line 161, in register
    hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_hooks.py", line 664, in __init__
    argnames, kwargnames = varnames(self.function)
                           ~~~~~~~~^^^^^^^^^^^^^^^
  File "C:\Git\Temp\.3.14\Lib\site-packages\pluggy\_hooks.py", line 313, in varnames
    sig = inspect.signature(
        func.__func__ if inspect.ismethod(func) else func  # type:ignore[arg-type]
    )
  File "C:\Users\bers\AppData\Roaming\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\inspect.py", line 3312, in signature       
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
           ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                   globals=globals, locals=locals, eval_str=eval_str,
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                   annotation_format=annotation_format)
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\bers\AppData\Roaming\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\inspect.py", line 3027, in from_callable   
    return _signature_from_callable(obj, sigcls=cls,
                                    follow_wrapper_chains=follow_wrapped,
                                    globals=globals, locals=locals, eval_str=eval_str,
                                    annotation_format=annotation_format)
  File "C:\Users\bers\AppData\Roaming\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\inspect.py", line 2502, in _signature_from_callable
    return _signature_from_function(sigcls, obj,
                                    skip_bound_arg=skip_bound_arg,
                                    globals=globals, locals=locals, eval_str=eval_str,
                                    annotation_format=annotation_format)
  File "C:\Users\bers\AppData\Roaming\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\inspect.py", line 2325, in _signature_from_function
    annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str,
                                  format=annotation_format)
  File "C:\Users\bers\AppData\Roaming\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\annotationlib.py", line 890, in get_annotations
    ann = _get_dunder_annotations(obj)
  File "C:\Users\bers\AppData\Roaming\uv\python\cpython-3.14.0-windows-x86_64-none\Lib\annotationlib.py", line 1059, in _get_dunder_annotations
    ann = getattr(obj, "__annotations__", None)
  File "C:\Git\Temp\conftest.py", line 7, in __annotate__
    def pytest_configure(config: Config) -> None: ...
                                 ^^^^^^
NameError: name 'Config' is not defined

Activity

  1. RonnyPfannschmidt commented on Nov 21, 2025

    @RonnyPfannschmidt
    Member

    This is a partial stdlib bug

    We need to use a different call to work around it

    As workaround for you i recommend unconditional usage of the public export name for config

  2. The-Compiler commented on Nov 21, 2025

    @The-Compiler
    Member

    More minimal reproducer:

    import inspect
    from typing import TYPE_CHECKING
    
    if TYPE_CHECKING:
        from _pytest.config import Config
    
    def pytest_configure(config: Config) -> None: ...
    
    inspect.signature(pytest_configure)

    results in:

    Traceback (most recent call last):
      File "[...]/x.py", line 9, in <module>
        inspect.signature(pytest_configure)
        ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.14/inspect.py", line 3312, in signature
        return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
               ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                       globals=globals, locals=locals, eval_str=eval_str,
                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                       annotation_format=annotation_format)
                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.14/inspect.py", line 3027, in from_callable
        return _signature_from_callable(obj, sigcls=cls,
                                        follow_wrapper_chains=follow_wrapped,
                                        globals=globals, locals=locals, eval_str=eval_str,
                                        annotation_format=annotation_format)
      File "/usr/lib/python3.14/inspect.py", line 2502, in _signature_from_callable
        return _signature_from_function(sigcls, obj,
                                        skip_bound_arg=skip_bound_arg,
                                        globals=globals, locals=locals, eval_str=eval_str,
                                        annotation_format=annotation_format)
      File "/usr/lib/python3.14/inspect.py", line 2325, in _signature_from_function
        annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str,
                                      format=annotation_format)
      File "/usr/lib/python3.14/annotationlib.py", line 890, in get_annotations
        ann = _get_dunder_annotations(obj)
      File "/usr/lib/python3.14/annotationlib.py", line 1059, in _get_dunder_annotations
        ann = getattr(obj, "__annotations__", None)
      File "[...]/x.py", line 7, in __annotate__
        def pytest_configure(config: Config) -> None: ...
                                     ^^^^^^
    NameError: name 'Config' is not defined

    Since pluggy doesn't need the annotations resolved, I think it just needs to pass annotation_format=annotationlib.Format.FORWARDREF (or Format.STRING) here. I don't think this is a stdlib bug.

  3. added a commit that references this issue on Nov 28, 2025
    51af9de
  4. added 4 commits that reference this issue on Apr 29, 2026
    62071e1
    de91e48
    417de3f
    ff29803
  5. added 2 commits that reference this issue on May 17, 2026
    ff60fb3
    80db10d
  6. added 2 commits that reference this issue on May 27, 2026
    74876c4
    5656caa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions