Search code examples
pythonpytestmypy

How to specify mypy type pytest_configure fixtures?


I am trying to specify mypy type hints for the pytest native fixtures I am using in my test project e.g.:

import pytest

def pytest_configure(config):
    # Do something useful here

The config fixture returns a _pytest.config.Config object. If I try to model this naively:

import pytest

def pytest_configure(config: Config) -> None:
    # Do something useful here

I receive a mypy error: conftest.py:3: error: Name 'Config' is not defined [name-defined]

I could do from _pytest.config import Config, but this doesn't seem to be a good way, because _pytest is private. Another option would be to ignore the type with # type: ignore. If this is the recommended way I would of course do this, but I wonder if there is a better option.

I have the same issues in with any kind of pytest native fixtures I use, e.g. request which is used for parameterized fixtures. This would be a _pytest.fixtures.FixtureRequest.


Solution

  • Importing from _pytest.config

    Since pytest doesn't currently export Config (as of 6.2), the only way for typing is to use from _pytest.config import Config. This is how I also type config, as can be seen e.g. in this question of mine:

    from _pytest.config import Config
    
    def pytest_configure(config: Config) -> None:
        ...
    

    You can track the typing progress in this pytest issue: #7469.

    Custom type stubs

    You can also introduce a small custom type stub that hides the reexport. It's questionable whether it will be useful here, only worth to mention for an alternative solution. If you create a file _typeshed/pytest.pyi with the following contents:

    from typing import Any
    from _pytest.config import Config as Config
    
    def __getattr__(name: str) -> Any: ...  # incomplete
    

    and make it accessible to mypy in mypy.ini:

    [mypy]
    mypy_path = _typeshed
    

    Now you can import from pytest import Config at least in type checking mode - the runtime import will still fail. So the imports would look like

    from typing import Any, TYPE_CHECKING
    
    if TYPE_CHECKING:
        from pytest import Config
    else:
        Config = Any
    
    
    def pytest_configure(config: Config) -> None:
        pass
    

    The only benefit of that solution is that the private import is now hidden; I'd still go with the private import though.