Must code guarded by `if TYPE_CHECKING` be valid Python code?

I have been looking into this issue that is concerned with executing code guarded by if TYPE_CHECKING. Eventually, I bumped into a line in Pydantic that can not be executed because of a TypeError: 'type' object is not subscriptable. I now have the following questions:

  • Where does the classmethod[Any, Any, Any] syntax come from? I guess it’s similar to how Callables can be parameterized but I couldn’t find any documentation.
  • That line was introduced into Pydantic when switching to Pyright. My experience with Pyright is that it is more lenient compared to Mypy but does that go as far as accepting invalid Python code?
  • Code guarded by if TYPE_CHECKING does not get executed by Python but does that mean that it doesn’t need to be executable? It does feel kind of weird for a tool like sphinx-autodoc-typehints to go and execute all the guarded code but I couldn’t find documentation on whether that’s actually discouraged or not. On the other hand, allowing any arbitrary code for the sake of static analysis because “it never gets executed anyway” seems like a slippery slope to me.

Any clarifications or pointers to documentation would be very welcome :slight_smile:

I’ve never seen classmethod[Any, Any, Any] myself.

Seems like this is a Pyright-specific question then? (I mostly have context on mypy.) If so, might be worth asking your question in that sub-community.

I’m not sure I understand the question. Anything inside of an if TYPE_CHECKING block is intended to only be visible to typecheckers. In particular it’s not intended to be visible to Python at runtime (beyond the syntax being valid).

Sounds like your question might be specific to sphinx-autodoc-typehints ? If so, might be worth asking your question in that sub-community.

I’m not sure I understand the question. Anything inside of an if TYPE_CHECKING block is intended to only be visible to typecheckers. In particular it’s not intended to be visible to Python at runtime (beyond the syntax being valid).

Still feels a bit weird that I could stick some tool-specific stuff that follows the Python syntax but makes no sense at runtime into an if TYPE_CHECKING block.

Sounds like your question might be specific to sphinx-autodoc-typehints ? If so, might be worth asking your question in that sub-community.

I did already :slight_smile: Answer from the maintainer here

Breaking import cycles with TYPE_CHECKING

Well, the most common reason I have to use an if TYPE_CHECKING block personally is to break import cycles between Python modules that only involve types. For example:

# a.py
import BType from b

class AType:
    child: BType
# b.py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import AType from a

class BType:
    parent: 'AType'

The import code inside the TYPE_CHECKING blocks above certainly has a valid interpretation inside Python. However it wouldn’t be possible to actually execute it in the Python runtime because when a.py started to import b.py and b.py then tried to import a.py, there would be an runtime error due to the circular dependency.

Using typechecker-specific functionality with TYPE_CHECKING

The only other case where I use TYPE_CHECKING blocks occasionally is to use a typechecker-specific syntax that doesn’t happen to work at runtime.

And the only such syntax I rely on occasionally is mypy’s internal support for the Ellipsis constant recognized as the builtins.ellipsis type:

# uses_ellipsis.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from builtins import ellipsis  # only recognized by mypy; won't work in regular Python

# Maps short filenames to a resolved full filepath, 
# or to None if file not found,
# or to Ellipsis if multiple full filepaths match
filepath_for_filename: dict[str, str | None | 'ellipsis']

(FWIW, I believe mypy in Python 3.10+ supports types.EllipsisType so I probably won’t need to use builtins.ellipsis once all the projects I work on have updated to Python 3.10+.)

Now, naturally by using a mypy-only builtins.ellipsis type I’m limiting the typechecker that can be used on my application to mypy only. But that’s acceptable for my use case. If I was instead writing a library, then I’d probably want to support multiple typecheckers, or at least mypy and pyright.