Skip to content

Allow bare typing.ClassVar qualifiers #1931

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions conformance/results/mypy/classes_classvar.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ Internal error if TypeVarTuple is used in ClassVar.
Does not reject use of ParamSpec in ClassVar.
Rejects ClassVar nested in Annotated.
Does not reject use of ClassVar in TypeAlias definition.
Does not infer type of ClassVar from assignment if no type is provided.
"""
output = """
classes_classvar.py:38: error: ClassVar[...] must have at most one type argument [valid-type]
Expand All @@ -15,24 +14,22 @@ classes_classvar.py:46: error: ClassVar cannot contain type variables [misc]
classes_classvar.py:52: error: Incompatible types in assignment (expression has type "dict[Never, Never]", variable has type "list[str]") [assignment]
classes_classvar.py:54: error: Variable should not be annotated with both ClassVar and Final [misc]
classes_classvar.py:55: error: Invalid type: ClassVar nested inside other type [valid-type]
classes_classvar.py:61: error: Invalid type: ClassVar nested inside other type [valid-type]
classes_classvar.py:63: error: ClassVar can only be used for assignments in class body [misc]
classes_classvar.py:64: error: ClassVar can only be used for assignments in class body [misc]
classes_classvar.py:65: error: ClassVar can only be used for assignments in class body [misc]
classes_classvar.py:67: error: ClassVar can only be used for assignments in class body [misc]
classes_classvar.py:67: error: Invalid type: ClassVar nested inside other type [valid-type]
classes_classvar.py:69: error: ClassVar can only be used for assignments in class body [misc]
classes_classvar.py:70: error: ClassVar can only be used for assignments in class body [misc]
classes_classvar.py:71: error: ClassVar can only be used for assignments in class body [misc]
classes_classvar.py:78: error: Expression is of type "Any", not "float" [assert-type]
classes_classvar.py:105: error: Cannot assign to class variable "stats" via instance [misc]
classes_classvar.py:134: error: Incompatible types in assignment (expression has type "ProtoAImpl", variable has type "ProtoA") [assignment]
classes_classvar.py:134: note: "ProtoAImpl" is missing following "ProtoA" protocol member:
classes_classvar.py:134: note: z
classes_classvar.py:134: note: Protocol member ProtoA.x expected class variable, got instance variable
classes_classvar.py:134: note: Protocol member ProtoA.y expected class variable, got instance variable
classes_classvar.py:73: error: ClassVar can only be used for assignments in class body [misc]
classes_classvar.py:77: error: ClassVar can only be used for assignments in class body [misc]
classes_classvar.py:111: error: Cannot assign to class variable "stats" via instance [misc]
classes_classvar.py:140: error: Incompatible types in assignment (expression has type "ProtoAImpl", variable has type "ProtoA") [assignment]
classes_classvar.py:140: note: "ProtoAImpl" is missing following "ProtoA" protocol member:
classes_classvar.py:140: note: z
classes_classvar.py:140: note: Protocol member ProtoA.x expected class variable, got instance variable
classes_classvar.py:140: note: Protocol member ProtoA.y expected class variable, got instance variable
"""
conformance_automated = "Fail"
errors_diff = """
Line 47: Expected 1 errors
Line 72: Expected 1 errors
Line 61: Unexpected errors ['classes_classvar.py:61: error: Invalid type: ClassVar nested inside other type [valid-type]']
Line 78: Unexpected errors ['classes_classvar.py:78: error: Expression is of type "Any", not "float" [assert-type]']
Line 78: Expected 1 errors
Line 67: Unexpected errors ['classes_classvar.py:67: error: Invalid type: ClassVar nested inside other type [valid-type]']
"""
2 changes: 1 addition & 1 deletion conformance/results/mypy/version.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version = "mypy 1.15.0"
test_duration = 1.2
test_duration = 2.1
24 changes: 12 additions & 12 deletions conformance/results/pyre/classes_classvar.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ classes_classvar.py:38:10 Invalid type parameters [24]: Generic type `CV` expect
classes_classvar.py:39:10 Invalid type [31]: Expression `typing.ClassVar[3]` is not a valid type.
classes_classvar.py:40:13 Unbound name [10]: Name `var` is used but not defined in the current scope.
classes_classvar.py:52:4 Incompatible attribute type [8]: Attribute `bad8` declared in class `ClassA` has type `List[str]` but is used as type `Dict[Variable[_KT], Variable[_VT]]`.
classes_classvar.py:65:8 Undefined attribute [16]: `ClassA` has no attribute `xx`.
classes_classvar.py:68:8 Incompatible return type [7]: Expected `CV[int]` but got `int`.
classes_classvar.py:78:0 Assert type [70]: Expected `float` but got `typing.Any`.
classes_classvar.py:105:0 Invalid assignment [41]: Assigning to class variable through instance, did you mean to assign to `Starship.stats` instead?
classes_classvar.py:134:0 Incompatible variable type [9]: a is declared to have type `ProtoA` but is used as type `ProtoAImpl`.
classes_classvar.py:66:4 Uninitialized attribute [13]: Attribute `good5` is declared in class `ClassA` to have type `typing.Any` but is never initialized.
classes_classvar.py:71:8 Undefined attribute [16]: `ClassA` has no attribute `xx`.
classes_classvar.py:74:8 Incompatible return type [7]: Expected `CV[int]` but got `int`.
classes_classvar.py:111:0 Invalid assignment [41]: Assigning to class variable through instance, did you mean to assign to `Starship.stats` instead?
classes_classvar.py:140:0 Incompatible variable type [9]: a is declared to have type `ProtoA` but is used as type `ProtoAImpl`.
"""
conformance_automated = "Fail"
errors_diff = """
Expand All @@ -28,11 +28,11 @@ Line 46: Expected 1 errors
Line 47: Expected 1 errors
Line 54: Expected 1 errors
Line 55: Expected 1 errors
Line 63: Expected 1 errors
Line 64: Expected 1 errors
Line 67: Expected 1 errors
Line 71: Expected 1 errors
Line 72: Expected 1 errors
Line 68: Unexpected errors ['classes_classvar.py:68:8 Incompatible return type [7]: Expected `CV[int]` but got `int`.']
Line 78: Unexpected errors ['classes_classvar.py:78:0 Assert type [70]: Expected `float` but got `typing.Any`.']
Line 69: Expected 1 errors
Line 70: Expected 1 errors
Line 73: Expected 1 errors
Line 77: Expected 1 errors
Line 78: Expected 1 errors
Line 66: Unexpected errors ['classes_classvar.py:66:4 Uninitialized attribute [13]: Attribute `good5` is declared in class `ClassA` to have type `typing.Any` but is never initialized.']
Line 74: Unexpected errors ['classes_classvar.py:74:8 Incompatible return type [7]: Expected `CV[int]` but got `int`.']
"""
2 changes: 1 addition & 1 deletion conformance/results/pyre/version.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version = "pyre 0.9.23"
test_duration = 6.9
test_duration = 3.9
16 changes: 8 additions & 8 deletions conformance/results/pyright/classes_classvar.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ classes_classvar.py:47:20 - error: "ClassVar" type cannot include type variables
classes_classvar.py:52:33 - error: Type "dict[Any, Any]" is not assignable to declared type "list[str]" (reportAssignmentType)
classes_classvar.py:54:17 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:55:17 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:63:26 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:64:12 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:65:18 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:67:26 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:71:8 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:72:20 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:105:14 - error: Cannot assign to attribute "stats" for class "Starship"
classes_classvar.py:69:26 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:70:12 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:71:18 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:73:26 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:77:8 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:78:20 - error: "ClassVar" is not allowed in this context (reportInvalidTypeForm)
classes_classvar.py:111:14 - error: Cannot assign to attribute "stats" for class "Starship"
  Attribute "stats" cannot be assigned through a class instance because it is a ClassVar (reportAttributeAccessIssue)
classes_classvar.py:134:13 - error: Type "ProtoAImpl" is not assignable to declared type "ProtoA"
classes_classvar.py:140:13 - error: Type "ProtoAImpl" is not assignable to declared type "ProtoA"
  "ProtoAImpl" is incompatible with protocol "ProtoA"
    "x" is defined as a ClassVar in protocol
    "y" is defined as a ClassVar in protocol (reportAssignmentType)
Expand Down
2 changes: 1 addition & 1 deletion conformance/results/pyright/version.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version = "pyright 1.1.398"
test_duration = 1.5
test_duration = 2.3
35 changes: 18 additions & 17 deletions conformance/results/pytype/classes_classvar.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Does not reject use of TypeVar in ClassVar.
Does not reject use of ParamSpec in ClassVar.
Does not reject use of ClassVar as a generic type argument.
Rejects initialization of ClassVar if no type argument is provided.
Does not infer ClassVar with no type argument and no assigned value as Any.
Does not reject use of ClassVar in parameter type annotation.
Does not reject use of ClassVar in local variable annotation.
Does not reject use of ClassVar in instance variable annotation.
Expand Down Expand Up @@ -85,27 +86,27 @@ classes_classvar.py:52:5: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in ClassA
bad8: ClassVar[list[str]] = {} # E: type violation in initialization
\u001b[1m\u001b[31m~~~~\u001b[39m\u001b[0m

classes_classvar.py:60:5: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in ClassA: Type annotation for good4 does not match type of assignment [annotation-type-mismatch]
classes_classvar.py:63:5: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in ClassA: Type annotation for good4 does not match type of assignment [annotation-type-mismatch]

good4: ClassVar = 3.1
\u001b[1m\u001b[31m~~~~~\u001b[39m\u001b[0m

classes_classvar.py:68:16: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in method2: bad return type [bad-return-type]
classes_classvar.py:74:16: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in method2: bad return type [bad-return-type]

return 3
\u001b[1m\u001b[31m~\u001b[39m\u001b[0m

classes_classvar.py:78:1: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in <module>: ClassVar [assert-type]
classes_classvar.py:84:1: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in <module>: ClassVar [assert-type]

assert_type(ClassA.good4, float)
\u001b[1m\u001b[31m~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[39m\u001b[0m
assert_type(ClassA.good5, Any)
\u001b[1m\u001b[31m~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[39m\u001b[0m

classes_classvar.py:124:5: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in ProtoA: Type annotation for z does not match type of assignment [annotation-type-mismatch]
classes_classvar.py:130:5: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in ProtoA: Type annotation for z does not match type of assignment [annotation-type-mismatch]

z: CV = [""]
\u001b[1m\u001b[31m~\u001b[39m\u001b[0m

classes_classvar.py:134:1: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in <module>: Type annotation for a does not match type of assignment [annotation-type-mismatch]
classes_classvar.py:140:1: \u001b[1m\u001b[31merror\u001b[39m\u001b[0m: in <module>: Type annotation for a does not match type of assignment [annotation-type-mismatch]

a: ProtoA = ProtoAImpl() # E: y is not a ClassVar
\u001b[1m\u001b[31m~\u001b[39m\u001b[0m
Expand All @@ -118,17 +119,17 @@ Line 46: Expected 1 errors
Line 47: Expected 1 errors
Line 54: Expected 1 errors
Line 55: Expected 1 errors
Line 63: Expected 1 errors
Line 64: Expected 1 errors
Line 65: Expected 1 errors
Line 67: Expected 1 errors
Line 69: Expected 1 errors
Line 70: Expected 1 errors
Line 71: Expected 1 errors
Line 72: Expected 1 errors
Line 105: Expected 1 errors
Line 73: Expected 1 errors
Line 77: Expected 1 errors
Line 78: Expected 1 errors
Line 111: Expected 1 errors
Line 7: Unexpected errors ['classes_classvar.py:7:1: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in <module>: typing.TypeVarTuple not supported yet [not-supported-yet]']
Line 29: Unexpected errors ['classes_classvar.py:29:6: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in <module>: Function TypeVarTuple.__init__ expects 1 arg(s), got 2 [wrong-arg-count]']
Line 60: Unexpected errors ['classes_classvar.py:60:5: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in ClassA: Type annotation for good4 does not match type of assignment [annotation-type-mismatch]']
Line 68: Unexpected errors ['classes_classvar.py:68:16: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in method2: bad return type [bad-return-type]']
Line 78: Unexpected errors ['classes_classvar.py:78:1: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in <module>: ClassVar [assert-type]']
Line 124: Unexpected errors ['classes_classvar.py:124:5: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in ProtoA: Type annotation for z does not match type of assignment [annotation-type-mismatch]']
Line 63: Unexpected errors ['classes_classvar.py:63:5: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in ClassA: Type annotation for good4 does not match type of assignment [annotation-type-mismatch]']
Line 74: Unexpected errors ['classes_classvar.py:74:16: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in method2: bad return type [bad-return-type]']
Line 84: Unexpected errors ['classes_classvar.py:84:1: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in <module>: ClassVar [assert-type]']
Line 130: Unexpected errors ['classes_classvar.py:130:5: \\x1b[1m\\x1b[31merror\\x1b[39m\\x1b[0m: in ProtoA: Type annotation for z does not match type of assignment [annotation-type-mismatch]']
"""
2 changes: 1 addition & 1 deletion conformance/results/pytype/version.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version = "pytype 2024.10.11"
test_duration = 35.9
test_duration = 43.3
8 changes: 4 additions & 4 deletions conformance/results/results.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,16 @@ <h3>Python Type System Conformance Test Results</h3>
<div class="table_container"><table><tbody>
<tr><th class="col1">&nbsp;</th>
<th class='tc-header'><div class='tc-name'>mypy 1.15.0</div>
<div class='tc-time'>1.2sec</div>
<div class='tc-time'>2.1sec</div>
</th>
<th class='tc-header'><div class='tc-name'>pyright 1.1.398</div>
<div class='tc-time'>1.5sec</div>
<div class='tc-time'>2.3sec</div>
</th>
<th class='tc-header'><div class='tc-name'>pyre 0.9.23</div>
<div class='tc-time'>6.9sec</div>
<div class='tc-time'>3.9sec</div>
</th>
<th class='tc-header'><div class='tc-name'>pytype 2024.10.11</div>
<div class='tc-time'>35.9sec</div>
<div class='tc-time'>43.3sec</div>
</th>
</tr>
<tr><th class="column" colspan="5">
Expand Down
10 changes: 8 additions & 2 deletions conformance/tests/classes_classvar.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,14 @@ class ClassA(Generic[T, P]):
good1: CV[int] = 1
good2: ClassVar[list[str]] = []
good3: ClassVar[Any] = 1
# > If an assigned value is available, the type should be inferred as some type
# > to which this value is assignable.
# Here, type checkers could infer good4 as `float` or `Any`, for example.
good4: ClassVar = 3.1
good5: Annotated[ClassVar[list[int]], ""] = []
# > If the `ClassVar` qualifier is used without any assigned value, the type
# > should be inferred as `Any`:
good5: ClassVar #E? Type checkers may error on uninitialized ClassVar
good6: Annotated[ClassVar[list[int]], ""] = []

def method1(self, a: ClassVar[int]): # E: ClassVar not allowed here
x: ClassVar[str] = "" # E: ClassVar not allowed here
Expand All @@ -75,7 +81,7 @@ def method2(self) -> ClassVar[int]: # E: ClassVar not allowed here
assert_type(ClassA.good1, int)
assert_type(ClassA.good2, list[str])
assert_type(ClassA.good3, Any)
assert_type(ClassA.good4, float)
assert_type(ClassA.good5, Any)


class BasicStarship:
Expand Down
2 changes: 1 addition & 1 deletion docs/spec/annotations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ The following grammar describes the allowed elements of type and annotation expr
annotation_expression: <Required> '[' `annotation_expression` ']'
: | <NotRequired> '[' `annotation_expression` ']'
: | <ReadOnly> '[' `annotation_expression`']'
: | <ClassVar> '[' `annotation_expression`']'
: | <ClassVar> ('[' `annotation_expression`']')?
: | <Final> ('[' `annotation_expression`']')?
: | <InitVar> '[' `annotation_expression` ']'
: | <Annotated> '[' `annotation_expression` ','
Expand Down
29 changes: 24 additions & 5 deletions docs/spec/class-compat.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,30 @@ Class type assignability

(Originally specified in :pep:`526`.)

A :term:`type qualifier` ``ClassVar[T]`` exists in the :py:mod:`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. This restriction is enforced by static checkers,
but not at runtime.
The :py:data:`typing.ClassVar` :term:`type qualifier` is used to annotate
class variables that should not be set on class instances. This restriction
is enforced by static checkers, but not at runtime.

:py:data:`~typing.ClassVar` may be used in one of several forms:

* With an explicit type, using the syntax ``ClassVar[<type>]``. Example::

class C:
x: ClassVar[float] = 1

* With no type annotation. Example::

class C:
y: ClassVar = 2
z: ClassVar

If an assigned value is available (e.g. with ``y``), the type should be
inferred as some type to which this value is :term:`assignable` (for example,
``int``, ``Literal[2]``, or ``Any``).

If the bare ``ClassVar`` qualifier is used without any assigned value, the type
should be inferred as :ref:`Any <any>`. Type checkers may error if no assigned
value is present.

Type annotations can be used to annotate class and instance variables
in class bodies and methods. In particular, the value-less notation ``a: int``
Expand Down