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 bare ClassVar
-
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 for ClassVar.
-
The syntax for type and annotations expressions does not seem to allow for a ClassVar without bracketed arguments, because it would fall under the type_expression branch and this only allows bare names if they "refer to a valid in-scope class, type alias, or TypeVar", but ClassVar 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 bare Final either, which is explicitly allowed in the spec This has actually been changed. A bare Final 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 both var: ClassVar[int] and var: ClassVar, because ClassVar 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 modify var through an instance.
ClassVar seems similar in spirit to Final, and a bare Final is explicitly allowed (but see below for why we probably don't want a bare ClassVar to have the same implication as a bare Final).
If allowed, what should the public type of C.var be?
A bare Final has the following meaning:
The 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 as Final[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 with var: 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).
Description
Consider a 'bare'
ClassVarannotation, i.e. without an explicitClassVar[…]type argument:There are two questions here:
C.varbe?Should this be allowed?
Arguments for why it should be disallowed:
PEP 526 which introduced
ClassVardoesn't mention the possibility of a bareClassVarThe typing spec seems rather clear that this is not allowed:
A bare
Finalis 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
ClassVarwithout bracketed arguments, because it would fall under thetype_expressionbranch and this only allows bare names if they "refer to a valid in-scope class, type alias, or TypeVar", butClassVaris a special form:Although to be fair, the syntax does not seem to allow a bareThis has actually been changed. A bareFinaleither, which is explicitly allowed in the specFinalis now explicitly allowed.Arguments for why it should be allowed:
var: int, or not to declare it at all. It therefore seems reasonable to allow bothvar: ClassVar[int]andvar: ClassVar, becauseClassVaris a type qualifier that adds information that is orthogonal to the declared type.ClassVarannotation in the sense that they raise a diagnostic if you try to modifyvarthrough an instance.ClassVarseems similar in spirit toFinal, and a bareFinalis explicitly allowed (but see below for why we probably don't want a bareClassVarto have the same implication as a bareFinal).If allowed, what should the public type of
C.varbe?A bare
Finalhas the following meaning: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 forFinalvariables, as they can not be modified.But for
ClassVar, this seems undesirable. If we treatC.varfrom above as having typeLiteral[1], we would not be allowed to modify it. There are two other reasonable behaviors:Unknown | Literal[1]as the public type, which would also be the type of an un-annotated class variableUnknownas 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 returningOption<Type<…>>instead ofType<…>in functions likeinfer_annotation_expression. It might also involve treatingvar: ClassVar = 1as a pure binding?If Option 2 seems like a possible alternative (for now?), we can simply remove some TODO comments (draft PR here).