Ah, there’s the crux. You are arguing from implementation convenience. But that’s trumped in most cases by user inconvenience, and for users it appears better not to have to worry about another weird keyword. (Plus, while you may not have to write any C code for a new reserved word, I assure you that the parser generator does generate code for it – quite a lot, I would expect. Ask @pablogsal.)
However, you also propose compile-time optimization. This appears to go against at least one use case (Sphinx apparently setting typing.TYPE_CHECKING to False.)
I also expect that the kind of code typically covered by an if __type_checking__: is likely just a few imports, and there’s not much benefit from eliminating those from the code – not executing them is the major gain here.
Maybe consider eliminating code only based on the -O flag?
What is the difference between keyword and unassignable name here?
Is there something user need to worry about here if __debug__ was keyword?
>>> def foo():
... __debug__ = 1 # not a builtin nor global name
...
File "<python-input-1>", line 2
__debug__ = 1
^^^^^^^^^
SyntaxError: cannot assign to __debug__
I already implemented it and I know 33 lines of generated C code in parser.
But my concern was language spec complexity. “If the implementation is hard to explain, it’s a bad idea.”
Convenience is only for 5 years, but language complexity is forever. That was why I chose keyword.
But it seems no one support me. I will implement non-keyword but unassignable (except False) builtin.
Several question here:
Should it allow assign only False, or allow assign anything?
If __type_checking__ = True is allowed, compile time elimination of if __type_checking__: block is really odd.
How about __type_checking__ = 0 or __type_checking__ = None?
How about f = False; __type_checking__ = f?
How about local variables? Should it syntax error like __debug__?
Keywords cannot be used as attribute or method names or local variables.
Changing the grammar is conceptually more impactful than adding a new builtin — alternate parsers need to learn about it (linters, formatters, type checkers). A new builtin is simpler for such tools.
I don’t see a strong reason for constraining assignments — assigning to other builtins is also a bad idea but we don’t enforce it.
The benefits of dead code elimination are IMO minimal in this case.
__debug__ is not keyword. But it can not be used too.
This is key point. If dead code elimination is not needed, we should just add TYPE_CHECKING to builtin, not __type_checking__. This minimizes transition costs.
On the other hand, I use if False: # TYPE_CHECKING a lot.
Users who are concerned about memory usage and pyc size will not be able to migrate to the new method, and eventually multiple methods will continue to be used.
Instead one file, I use whole SQLAlchemy 2.0.39 to estimate how code elimination works:
Python 3.14 is 13.5% faster on startup. Congrats to PEP 649!
But it increase about 10% maxrss and pyc size…
if False: doesn’t affect to import time.
if False: reduces maxrss by only 1.84% compared to if TYPE_CHECKING:
if False: reduces pyc size by only 3.26%
OK. Now I completely agree with you.
I will rewrite the PEP to just add a TYPE_CHECKING=False builtin. It is just a variable, not a constant.
It doesn’t break from typing import TYPE_CHECKING nor TYPE_CHECKING = False.
Because I could not find this in the discussion so far, I have to mention for the protocol that I have to disagree with that assessment. Introducing __type_checking__ as a constant in the __builtins__ module would be possible by adding the line
SETBUILTIN("__type_checking__", Py_False);
to Python/bltinmodule.c, so it is very simple. Adding a keyword would additionally require changes to the lexer and the parser. The literal False is implemented actually in both ways: it is a keyword, but simultaneously a member of __builtins__ that can be loaded using getattr(__builtins__, 'False'). The latter stems from Python 2 before False actually became a keyword.
I meant unassignable builtin, like __debug__.
In case of __debug__, all assignment are prohibited.
If we prohibit assignment to __type_checking__ excpet __type_checking__ = False, it is much more complex.
Since I can not use method same to __debug__, I need to implement check in many places (e.g. case [__type_checking__]:) It was too difficult to me.
I think for backward compatibility it’s important that it’s the same as before. That way in most code you only have to change the import, not any code that uses it. Also one could ask why it needs to be a dunder.
With the previous motivation it was useful to be a dunder, but now it isn’t, IMO.
It can be a regular builtin just line NotImplemented and Ellipsis.
I’d repeat @srittau’s argument here, which I agree with completely, but the question of naming is much more marginal if we’re just adding a new name to builtins.
This would be the only UPPER_CASE builtin, right? That sounds a bit unfortunate. And how much compatibility do we need anyway? Do people use it a lot right now?
In Flask, Jinja, Click, etc, I’m totally willing to add one of the compat examples given in this thread (globals()..., try/except, etc) to take advantage of this. Click especially will benefit a great deal from not having to import typing at runtime at all (and from deferred annotations), as startup time is especially important to CLI tools. I like how __type_checking__ matches __debug__ (which we also use in Click), but I’m not strongly opposed to any particular spelling.
@methane: Thanks for the clarification. Certainly, I fully agree with that.
@AA-Turner: I am not sure what makes the constant a special Python builtin? Most other builtins are not dunder-names. dir(__builtins__) gives me the following dunders:
__doc__, __loader__, __name__, __package__, __spec__. which are provided by the import mechanism;
__import__, __build_class__ and __debug__
@pitrou: Yes, it would be the only UPPER_CASE builtin.
If the builtin is not assignable, there’s no compatibility path. For example, __debug__ is not assignable, which means this code always raises an exception that can only be handled as an ImportError:
This means people can’t add their own TYPE_CHECKING = False or from typing import TYPE_CHECKING in order to provide a value at runtime.[1]
So please, just make it a regular value in the builtins namespace, like __name__, and not like __debug__ or False.
This example will also always raise a SyntaxError on versions where TYPE_CHECKING is an unassignable like __debug__ (and it raises if you use __debug__ today), even if the code is never executed:
if sys.version_info[:2] < (3, 14):
TYPE_CHECKING = False
Assuming it keeps that name. Personally I prefer the dunder, if it’s going to be set unconditionally at runtime, but it’s not as big a deal as it being a syntax error to assign to it. ↩︎
If someone wants the maximized optimization that could come from making __type_checking__ not assignable, couldn’t they get that by putting the if __type_checking__ inside an if __debug__ ?
if __debug__:
if __type_checking__:
...
Would that make it so none of this creates any byte code?
Unless it is specified for type checkers to special case the expression if False and __type_checking__:, it should be if False or __type_checking__:. Without it being a non-reassignable constant, this can’t be optimized out. The first form a typechecker could reasonably never even see that the symbol in question is __type_checking__ if it short circuits checking the condition to match runtime (as it should for evaluating reachability of an expression under normal circumstances) without such a special specification case. As part of prior discussions on this, type checker authors did not want to special case things on this, so there might be pushback on such a special case.
Could an if __debug__: block be used for older Python versions? I don’t believe type checkers treat __debug__ as always being False so they should analyze the contents of the block and __debug__ will be False when run with python -O – which should be used if performance is critical.
I have qualms about the widespread use of if __type_checking__: blocks (however they are spelled) because I hope that runtime type information becomes more widely used. I can easily envision an interactive prompt that makes use of runtime types to provide completions, warnings, etc. But, I also recognize that runtime types are not widely used and that practicality may be more important than purity in this case.