-
Notifications
You must be signed in to change notification settings - Fork 216
Description
Description
Consider a 'bare' ClassVar annotation, i.e. without an explicit ClassVar[…] type argument:
class C:
var: ClassVar = 1There are two questions here:
- Should this be allowed?
- If allowed, what should the public type of
C.varbe?
Should this be allowed?
Arguments for why it should be disallowed:
-
PEP 526 which introduced
ClassVardoesn'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
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: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 bareFinaleither, which is explicitly allowed in the specFinalis 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, becauseClassVaris a type qualifier that adds information that is orthogonal to the declared type. - Mypy and Pyright both support a bare
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.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
Unknownas 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).