PEP 781: Make ``TYPE_CHECKING`` a built-in constant

Abstract

This PEP proposes adding a new built-in variable, :data:!TYPE_CHECKING, to improve
the experience of writing Python code with type annotations. It is evaluated
as True when the code is being analyzed by a static type checker, and as
False during normal runtime execution. Unlike :data:typing.TYPE_CHECKING,
which this variable replaces, it does not require an import statement.

Motivation

Type annotations were defined for Python by :pep:484, and have enjoyed
widespread adoption. A challenge with fully-annotated code is that many
more imports are required in order to bring the relevant name into scope,
potentially causing import cycles without careful design. This has been
recognized by :pep:563 and later :pep:649, which introduce two different
mechanisms for deferred evaluation of type annotations. As PEP 563 notes,
ā€œtype hints are … not computationally freeā€. The :data:typing.TYPE_CHECKING
constant was thus introduced__, initially to aid in breaking cyclic imports.

__ Add a constant that's False at runtime but True when type checking Ā· Issue #230 Ā· python/typing Ā· GitHub

In situations where startup time is critical, such as command-line interfaces,
applications, or core libraries, programmers may place all import statements
not required for runtime execution within a ā€˜TYPE_CHECKING block’, or even
defer certain imports to within functions. The typing module itself though
can take as much as 10ms to import, longer than Python takes to initialize.
The time taken to import the typing module clearly cannot be ignored.

To avoid importing TYPE_CHECKING from typing, developers currently
define a module-level variable such as TYPE_CHECKING = False or use code
like if False: # TYPE_CHECKING. Providing a standard method will allow
many tools to implement the same behavior consistently. It will also allow
third-party tools in the ecosystem to standardize on a single behavior
with guaranteed semantics, as for example some static type checkers currently
do not permit local constants, only recognizing typing.TYPE_CHECKING.

Specification

TYPE_CHECKING is a built-in constant and its value is False.
Unlike True, False, None, and __debug__, TYPE_CHECKING is
not a real constant; assigning to it will not raise a SyntaxError.

Static type checkers must treat TYPE_CHECKING as True, similar to
:data:typing.TYPE_CHECKING.

If this PEP is accepted, the new TYPE_CHECKING constant will be
the preferred approach and importing typing.TYPE_CHECKING will be
deprecated.

To minimize the runtime impact of typing, this deprecation will generate
DeprecationWarning no sooner than Python 3.13’s end of life, scheduled
for October 2029.

Instead, type checkers may warn about such deprecated usage when the target
version of the checked program is signalled to be Python 3.14 or newer.

Backwards Compatibility

Since TYPE_CHECKING doesn’t prohibit assignment, existing code using
TYPE_CHECKING will continue to work.

# This code will continue to work
TYPE_CHECKING = False
from typing import TYPE_CHECKING

User can remove the assignment to TYPE_CHECKING after they stop using
Python 3.13 or older versions.

How to Teach This

  • Use if TYPE_CHECKING: for skipping type-checking code at runtime.
  • Use from typing import TYPE_CHECKING to support Python versions before 3.14.
  • Workarounds like TYPE_CHECKING = False or if False: # TYPE_CHECKING
    will continue to work, but are not recommended.

Reference Implementation

  • python/cpython#131793 <https://github.com/python/cpython/pull/131793>__

Rejected Ideas

Eliminate type-checking-only code

It is considered to add real constant named __type_checking__
to eliminate type-checking-only code at compile time.

However, adding real constant to language increase complexity of the language.
Benefit from eliminating type-checking-only code is estimated to be not enough
to justify the complexity.

Optimize import typing

Future optimizations may eliminate the need to avoid importing the typing
module for startup time.

Even with such optimizations, there will still be use cases where minimizing
imports is beneficial, such as running Python on embedded systems or
in browsers.

Therefore, defining a constant for skipping type-checking-only code outside
the typing module remains valuable.

Copyright

This document is placed in the public domain or under the
CC0-1.0-Universal license, whichever is more permissive.

31 Likes

Makes sense to me.

I wish we could improve import speeds enough to where this wouldn’t be a concern, but alas we are where are.

5 Likes

Will typing.TYPE_CHECKING stay as a functionally equivalent solution or will it be discouraged/deprecated? Two equivalent solutions are not ideal. While it’s not something we can get rid of in short term, IMHO the PEP should define the long-term vision.

I think deprecation should only start after the minimum supported python version has __type_checking__. After that it should be easy for linters like ruff to upgrade TYPE_CHECKING to __type_checking__.

2 Likes

While having two methods is not ideal, `typing.TYPE_CHECKING’ is a single constant and does not require significant maintenance. I believe there is no need to remove it at the expense of backward compatibility.

By changing the definition of typing.TYPE_CHECKING from TYPE_CHECKING = False to TYPE_CHECKING = __type_checking__, future type checkers can only consider __type_checking__.

I will add these two points to the PEP.

5 Likes

Depending on your point of view, they are not functionally equivalent since ignoring imports can have substantial effects on startup time. Also, having two ways to do things is not really a problem when one of them is superior, which I believe this proposal is.

1 Like

To be clear: I know they have different background effects, and that typing.TYPE_CHECKING will need to be available for a long time. My point is that we should give clear guidance when to use what and what to expect in the future. This is important because it will influence what people will generally use once __type_checking__ will become available. I’m fine with either of the following statements, but I think we should add them to the PEP.

When to use what:

  • Prefer __type_checking__ if possible:

    Use typing.TYPE_CHECKING if your code must be compatible with Python < 3.14, otherwise use __type_checking__.

  • Prefer typing.TYPE_CHECKING if possible:

    Generally use typing.TYPE_CHECKING. You may use __type_checking__ in Python > 3.14 if import time is a concern.

  • No preference:

    Use typing.TYPE_CHECKING or __type_checking__ as you see fit.

What to expect in the future:

  • typing.TYPE_CHECKING and __type_checking__ will stay as equal options.
  • typing.TYPE_CHECKING is discouraged (use __type_checking_ if possible), but will stay for the foreseable future.
  • typing.TYPE_CHECKING may become deprecated once all supported python versions have gained __type_checking__.
3 Likes
  • Prefer __type_checking__ if possible:

and

  • typing.TYPE_CHECKING is discouraged (use __type_checking_ if possible), but will stay for the foreseable future.

I will add it. Thank you.

4 Likes

What is the behavior of __type_checking__ = True or __type_checking__ = False in module code?

What is the recommended method for declaring a type-checking-only block in a module supporting Python 3.13?

Will it be possible to mock, for example, when importing modules for Sphinx autodoc?

1 Like

If implemented as a keyword, trying to assign will raise a SyntaxError.

As currently, typing.TYPE_CHECKING must be used if you want to support Python < 3.14.

Mocking a keyword is not possible. See Specify `TYPE_CHECKING = False` without typing import - #41 by AA-Turner and following for a discussion on whether there’s a need for that.
On a side-note: This might be a reason to keep typing.TYPE_CHECKING indefinetly for execptional use cases.

3 Likes

I’ve read through and reviewed the PEP. I support the idea, and hopefully it can lead to smaller bytecode, faster load times, and allows us to (eventually) remove thousands of from typing import TYPE_CHECKING and TYPE_CHECKING = False statements!

I’ve proposed some changes to (hopefully) make the benefits of standardisation around a builtin name clearer, as well as emphasising the performance improvements both in terms of time and size.

As with @timhoffm, I’m not sure that the argument in the PEP about implementing __type_checking__ as a keyword is the strongest. For the period between 3.14 and 3.19, especially for libraries, permitting __type_checking = False would be very useful. If it isn’t allowed, I’d expect to see a discussion of this in Rejected Ideas.

If implementing as a keyword is chosen, this would be a hard breaking change for any previous use of the name. I think this is probably fine, given it is a dunder name, but it should be mentioned in the PEP as a consideration. I’ve conducted a search on GitHub and it appears that there is no usage of the name currently as a variable, and only one use in any project at all (as a module name for type checking).

It may be useful to survey the impact on runtime type checkers (e.g. @Jelle’s pyanalyze, or typeguard, beartype, etc). I don’t know if these projects attempt to extract statements under TYPE_CHECKING currently, but under this proposal runtime alteration of the value would become a SyntaxError.

Speaking for Sphinx, as one of the few users of TYPE_CHECKING = True, we will adapt if this PEP is accepted – there is a long term ambition to move to static based (CST/AST) introspection for autodoc, but just a lack of resource to do so – please don’t block the PEP on us.

A

7 Likes

It’s a nice idea, but I won’t be able to use it as currently specified.

The library I’m working on supports Python 3.8 ~ 3.14.

I doubt that the keyword would be introduced to the old Python series, which means I can’t reference the new keyword, and it can’t be hacked around with something like __type_checking__ = False.

And if my library continues to import typing, the typing module will get imported by any app that uses my library, directly or transiently.

Given other libraries stuck in the same boat, I wonder how usable this proposal would be if approved.

P.S. see below for practical work-arounds

Someone correct me if I’m wrong, but couldn’t a package be published (call it dunder_type_checking or pep781) which just looks like:

import sys

if sys.version_info < (3, 14):
    from typing import TYPE_CHECKING
else:
    TYPE_CHECKING = __type_checking__   

(With some bike shedding on names and stuff)

Alternatively your code could do this as well

3 Likes

A slightly simpler version:

try:
    __type_checking__
except NameError:
    __type_checking__ = False

Almost all names from typing can be placed behind a TYPE_CHECKING block. For me, the benefit from this proposal comes from both the standardisation of a single name (avoiding the debate over using a module-local TYPE_CHECKING = False) and from the performance benefits. Some applications won’t need to make any changes, but I argue having the choice to be able to do so is an improvement.

See also @mikeshardmind’s thread (Current blockers on runtime deferal of typing imports) that has discussion on the (relatively few) names that are required at runtime, and ways of working around this.

A

5 Likes

An even simpler one:

__type_checking__ = False

Since __type_checking__ will never be True in any context when the assignment is going to be executed, and static type checkers will either ignore or complain about the assignment equally (and should be trained to ignore assignments to this variable).

Though ultimately I don’t think this makes that big a difference. The real cost comes from evaluating unused annotations at runtime, and until we can avoid that, the cost of importing typing to get False is irrelevant. There are other imports that cost more (e.g. enum and dataclasses seem overly expensive for what they do, while typing is comparatively cheap).

2 Likes

It would be nice to see improvements there as well but in those cases the issue is different since they are actually used at runtime. The typing module is pure overhead at runtime.

3 Likes

This is the version I think everyone prefers (see my first post above), but the current PEP disallows it by virtue of making __type_checking__ a keyword. I think wires were crossed here a little, sorry – hopefully if @methane agrees then we can update the PEP to allow this better form.

Beyond Oscar’s point, I think the PEP could be updated to mention that users of TYPE_CHECKING often (almost always?) also use from __future__ import annotations which avoids evaluation of any annotation. I haven’t timed the 3.14 changes with PEP 649/749, though, but I am aware that importing typing itself has become slower, I believe due to annotationlib.

A

2 Likes

A real keyword would break all the other examples as well (the parser will reject assigning to a keyword - try True = 2). You’d need:

setattr(__builtins__, "__type_checking__", False)

Having it not be a keyword is about the only chance of letting code be compatible between versions. We learned this (or should’ve) when making async a normal keyword instead of a contextual one (match is done correctly).

2 Likes

Keyword doesn’t break Josh’s sample code:

And simpler version:

try:
    TYPE_CHECKING = __type_checking__
except NameError:
    TYPE_CHECKING = False

By using keyword, I need to write any C code. I just need modify only python.gram.

On the other hand, __debug__ is implemented in ast_opt.c and symtable.c

Allowing only __type_checking__ = False makes it more complex than __debug__.
All Python runtimes need to pay the cost for complexity.

I will remove ā€œUsing Keywordā€ section from Rationale and add ā€œAllow __type_checking__ = Falseā€ in Rejected Ideas section.

My question is why not optimize the loading of the typing module. I think .py modules have been converted to C extension modules in the past in order to speed things up and I suspect the contents of the typing module aren’t changing very much.

1 Like