I realized that if we want this, we need to do it before the betas (since it adds a new public symbol typing.NoDefault
), so I implemented it: gh-116126: Implement PEP 696 by JelleZijlstra · Pull Request #116129 · python/cpython · GitHub.
There’s another problem with the current implementation: the default is lazily evaluated, but for generic classes, the default currently gets evaluated while the class is being created. Therefore, with my current implementation, you cannot use a forward reference in the default for a generic class.
>>> class X[T = Forward]: pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
class X[T = Forward]: pass
File "<stdin>", line 1, in <generic parameters of X>
class X[T = Forward]: pass
File "/Users/jelle/py/cpython/Lib/typing.py", line 1391, in __init__
self.__parameters__ = _collect_parameters(args)
~~~~~~~~~~~~~~~~~~~^^^^^^
File "/Users/jelle/py/cpython/Lib/typing.py", line 293, in _collect_parameters
if t.__default__ is not NoDefault:
^^^^^^^^^^^^^
File "<stdin>", line 1, in T
class X[T = Forward]: pass
^^^^^^^
NameError: name 'Forward' is not defined
We made the default lazily evaluated so that users can freely use forward references in default expressions, and also so that there is no runtime cost for having complicated expressions in the default.
To fix this, I’d like to add a new method TypeVar.has_default()
(also for ParamSpec
and TypeVarTuple
). This method returns a boolean depending on whether or not the type parameter has a default set.
Alternatives I considered:
- The check that throws an error is checking for certain conditions that we would also throw SyntaxErrors with the native syntax, so we could omit these checks for classes created using the native syntax (and keep them only for classes created with
Generic[]
). However, there are several other places intyping.py
that need to check whether a default exists without caring what the default is. It would be better if those places could perform this check without forcing evaluation of the default. - We could implement this check in Python with something like
try: return t.__default__ is not NoDefault except Exception: return True
: if evaluation throws an exception, that must mean there was a default. But this is hard to explain, and still unnecessarily forces evaluation of the default. - Once PEP 649 lands, we could evaluate the default with
inspect.SOURCE
semantics (PEP 649 – Deferred Evaluation Of Annotations Using Descriptors | peps.python.org), and avoid the exception. However, PEP 649 has not yet landed, and even if it does land in time for the PEP 696 implementation to depend on it, this solution has much the same problems as the previous one.
I expect checking whether a type parameter has a default to be a fairly common operation for libraries that do runtime typing introspection, so it would be useful to provide a convenient way to do it.
I’m about to start writing the code for this new method, but let me know if you disagree with this proposal or would like a different spelling than .has_default()
.
It feels kinda weird to me that we created a new sentinel AND a new method for the sole purpose of checking the existence of a default value. I would expect to need only one or the other.