Description
Description
Consider a 'bare' ClassVar
annotation, i.e. without an explicit ClassVar[…]
type argument:
class C:
var: ClassVar = 1
There are two questions here:
- Should this be allowed?
- If allowed, what should the public type of
C.var
be?
Should this be allowed?
Arguments for why it should be disallowed:
-
PEP 526 which introduced
ClassVar
doesn't mention the possibility of a bareClassVar
-
The typing spec seems rather clear that this is not allowed:
A type qualifier
ClassVar[T]
exists in the typing module. It accepts only a single argument that should be a valid type, and is used to annotate class variables that should not be set on class instances. -
A bare
Final
is explicitly allowed in the spec, but no such clarification exists forClassVar
. -
The syntax for type and annotations expressions does not seem to allow for a
ClassVar
without bracketed arguments, because it would fall under thetype_expression
branch and this only allows bare names if they "refer to a valid in-scope class, type alias, or TypeVar", butClassVar
is a special form:annotation_expression ::= … | <ClassVar> '[' annotation_expression ']' | <Final> ('[' annotation_expression']')? | … | type_expression type_expression ::= … | name (where name must refer to a valid in-scope class, type alias, or TypeVar) | …
Although to be fair, the syntax does not seem to allow a bareThis has actually been changed. A bareFinal
either, which is explicitly allowed in the specFinal
is now explicitly allowed.
Arguments for why it should be allowed:
- It is okay to declare the type of a (class) variable using
var: int
, or not to declare it at all. It therefore seems reasonable to allow bothvar: ClassVar[int]
andvar: ClassVar
, becauseClassVar
is a type qualifier that adds information that is orthogonal to the declared type. - Mypy and Pyright both support a bare
ClassVar
annotation in the sense that they raise a diagnostic if you try to modifyvar
through an instance. ClassVar
seems similar in spirit toFinal
, and a bareFinal
is explicitly allowed (but see below for why we probably don't want a bareClassVar
to have the same implication as a bareFinal
).
If allowed, what should the public type of C.var
be?
A bare Final
has the following meaning:
ID: Final = 1The typechecker should apply its usual type inference mechanisms to determine the type of
ID
(here, likely,int
). Note that unlike for generic classes this is not the same asFinal[Any]
.
This suggests that the type should be inferred from the right-hand side of the definition. For Red Knot, this would mean that we infer Literal[1]
which is both precise and correct for Final
variables, as they can not be modified.
But for ClassVar
, this seems undesirable. If we treat C.var
from above as having type Literal[1]
, we would not be allowed to modify it. There are two other reasonable behaviors:
- Use
Unknown | Literal[1]
as the public type, which would also be the type of an un-annotated class variable - Use
Unknown
as the public type, which would also be the public type of a class variable annotated withvar: ClassVar[Unknown]
Option 1 is the behavior that is documented as being desirable in TODO comments ([1], [2]). Option 2 is our current behavior on main
.
Implementing Option 1 is quite involved, as there is a difference (inconsistency?) between undeclared variables and variables declared with Unknown
(as documented here. It would probably involve returning Option<Type<…>>
instead of Type<…>
in functions like infer_annotation_expression
. It might also involve treating var: ClassVar = 1
as a pure binding?
If Option 2 seems like a possible alternative (for now?), we can simply remove some TODO comments (draft PR here).
Activity