Specify `TYPE_CHECKING = False` without typing import

How about introducing a global reserved __type_checking__ name, instead?

17 Likes

I think the pragma-like comment makes a lot of sense. It doesn’t involve reserving arbitrary names, and since it’s present within the file rather than config, it should work for libraries once standarized.


To expand on that:
I made a library which, among other things, provides two types, Some and Null (Null being an Enum).
I made those types explicitly inherit from an _Option protocol to ensure common interface.
Since Protocol and Enum use metaclasses, at runtime this initially resulted in an exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".\src\monads\option.py", line 130, in <module>
    class Null(_Option[Never], Enum):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

I alleviated that with a hack.[1]

class Null(_Option[Never] if TYPE_CHECKING else Generic[T], Enum):

  1. Initial attempt used object in the else branch, which in turn caused MRO issues. Generic hacks the hack. ↩︎

Would this be a sufficient reason to introduce an import type or similar from typescript?

Syntax side, I could be wrong, but it seems like marking imports as being type level imports could make it easy for the runtime to ignore them.

I wonder if you could even backport the behavior using a __future__ import.

Apologies in advance for what is likely a bad idea.

How penalizing would it be (and also how bad of a precedent) to have the runtime check in the import machinery:

  • (inside the “from X import Y” machinery)
  • is Y one element long?
  • is X typing?
  • is Y TYPE_CHECKING?
  • just expose the constant (set to False obviously)

For EVERY import that doesn’t match, most would incur the length check (should be unmeasurably fast), except for ALL imports of length one, in which case it’s a string equivalence check of (up to) less than 10 characters. And finally if you’re importing something else out of (slow) typing module, two string checks.

The only regression that comes to mind is that “typing” hasn’t actually been loaded, so all the downstream code shouldn’t run (e.g. don’t put it in sys.modules).

1 Like

You can implement that yourself by overwriting __import__ globally and see if it has any surprising side effects. (Ofcourse, this would be slower than a direct C level implementation)

When using micropython with stubs, I wanted to use typing.Literal but could not as typing is not available on micropython.
A solution is to import typing in an if TYPE_CHECKING block and use forward reference in the code.
So having a way to reference the TYPE_CHECKING in code without importing typing is important for the typed micropython ecosystem.
Also PEP 749 is very likely to never be implemented in micropython.

6 Likes

An idea to address performance issues might be to import typing in an if __debug__: block and then run the script with python -O when using it (as opposed to developing it).

I like the idea but absolutely hate the solution. I specifically agree with Eric when he writes:

Though disagree with pretty much everything else he writes. Eric, it might behoove you to be a bit more open minded to other perspectives and use some empathy when interfacing with others. This is something I have noticed you routinely forgetting to do in your discussions.

I think that reducing startup time is absolutely worth it. It might seem extreme, but I wouldn’t be opposed to just adding a new soft keyword. The type statement added to python shows that the language is willing to go pretty far into adjusting the language just for typing. Giving some special consideration to access some variable or anything to determine whether type checking is enabled doesn’t seem too far out there.

4 Likes

Here’s a wild idea:

if __name__ == "__typing__":
    ...

(take this half-seriously :D)

4 Likes

wicked. i propose

match __name__:
   case "__main__":
        main()
    case "__typing__":
        from …

that’s down to a quarter.

that would be useful in the context of multiprocessing, where you’d have

match __name__:
    case "__main__":
        main()
    case "__mp_main__":
        child_main()
    case "__typing__":
        from ...
1 Like

Thanks all! This invited more discussion that I expected, for something I thought would be somewhat niche

Despite both mypy and pyright currently supporting it, no one seemed thrilled about my suggestion to encode type checker only special casing of TYPE_CHECKING name in the typing spec (I’m curious if @erictraut plans to remove this support)

The most popular suggestions on this thread all seemed to involve runtime suggestions (like changing builtins). As I said in my initial post, I’m not a fan of this. If someone wishes to pursue it, it’d need to be a larger discussion and probably a PEP

I’m therefore not taking any actions here. If someone in the future wants to pursue making the type checker only special casing official, you’d have to make a PR to the spec and open an issue on GitHub - python/typing-council: Decisions by the Python Typing Council

4 Likes

I only use mypy so if it’s not added to the spec, I hope at least mypy doesn’t remove support for TYPE_CHECKING = False, because as noted above, it’s useful for CLIs and small scripts that want to minimise the total number of imports, especially as it’s getting slower to import typing in 3.14. Otherwise I fear some projects may remove type checking.

4 Likes

I prefer adding __type_checking__ constant, not builtin.

For example, SQLAlchemy is large library, and uses a lot of if TYPE_CHECKING:.
See elements.py for example.

When replacing if (typing\.)?TYPE_CHECKING with if False, bytecode size reduced about 4%.

$ wc -c *.pyc
  211087 elements.cpython-313.pyc  # original
  192163 elements_false.cpython-313.pyc  # replaced with `if False`

But if False is not looks good for readability.
This expression does not clearly express the intention.

Adding constant allow us to add rich type hints with zero runtime cost.

2 Likes

Can I start writing PEP for _type_checking__ using this thread as the Discussion, or should I create a new thread before writing the PEP?

1 Like

I am thinking about the details of __type_checking__.

Would that be implementable as a soft keyword?
The dunder method is reserved, and I don’t see it being used when I search at Sourcegaraph, so I don’t think it would break a lot of code to make it a keyword.

Also, would it be a good idea to treat __type_checking__ = False as a no-op instead of an error, making it easier to write code compatible with Python versions up to 3.13?

Since by design it can be used as an expression, it IMO shouldn’t be - unless you want reassignment to be a meaningful operation. Otherwise it’s a confusing situations that some people are going run into.

Since all dunder names are reserved we can pretty freely use it. Backwards compatibility is not something we should worry ourself with here.

I think “verify that False” is a better choice. Cost should still be neglible and it again prevents confusion where people try to assign true to it but that doesn’t work.

2 Likes

Write a PEP that summarizes the arguments made in this thread, then open a new thread once the PEP has been merged into the python/peps repo. The idea is that people reviewing the PEP won’t have to re-read all of the previous discussions that led up to it; it’s the PEP’s job to make sure the discussion is summarized well.

5 Likes

Most Python builtins can be freely overriden. I wouldn’t write this code myself but note e.g. sphinx/sphinx/ext/autodoc/importer.py at d066c2be731df5f1ffed5d657c696b57f39a4f39 · sphinx-doc/sphinx · GitHub

Assigning to __debug__ fails, which might be the inspiration here. I imagine it would also be relevant for implementing compiler optimisations, and @methane mentioned bytecode size above. Allowing __type_checking__ = False (and updating type checkers to recognise it) would mean that adoption could be much quicker, though.

I suppose the question is how important/useful is the invariant of __type_checking__ is False. I implemented the code that @hauntsaninja linked to as a (mildly awful) workaround for trying to identify the source of annotations with PEP 563 [1], where reloading a module after an initial import with TYPE_CHECKING = True can work and mean that we’re able to resolve more type annotations at runtime (as Sphinx imports the code that it is auto-documenting).

I’m not aware of many other use-cases for setting TYPE_CHECKING to True at runtime, though, so it may be that the gains from specifying the new constant as always False outweigh the drawbacks.

A


  1. I haven’t had time yet to properly investigate, but my hope is that PEP 649/749 makes things better ↩︎

1 Like