Bare ClassVar
annotation (original) (raw)
February 22, 2025, 11:30am 1
Should bare ClassVar
annotations be allowed?:
from typing import ClassVar
class A:
x: ClassVar = 1
reveal_type(A.x) # int
Some references:
Final
is explicitly allowed as a bare annotation in the spec (the grammar was recently updated).- Supported by the CPython implementation since bpo-46553: allow bare typing.ClassVar annotations by GBeauregard · Pull Request #30983 · python/cpython · GitHub.
- Both mypy and pyright support it, although mypy does not infer the type from the assignment, while it does for
Final
(as per the spec). Playgrounds: pyright, mypy. - Ruff issue, giving more details.
The least path of resistance seems to align the ClassVar
behavior with Final
. Thoughts?
mikeshardmind (Michael H) February 22, 2025, 3:09pm 2
Aligning it being allowed is probably a good thing here, but ruff as a linter[1] might still want to flag this (as it does for many other annotations) as this could result in inference-defined (allowed to diverge across type checkers) behavior in an exported type.
- I’m aware the issue is actually for red-knot, the in-progress type checker, but answering holistically about what is allowed in the type system versus flaggable for a reason. ↩︎
Jelle (Jelle Zijlstra) February 22, 2025, 3:54pm 3
Should the inferred type be int
or Literal[1]
? I think the reason it makes sense to allow bare Final
is that it’s sensible to always infer the narrowest type, but with ClassVar
that’s not the case.
mikeshardmind (Michael H) February 22, 2025, 4:32pm 4
If (and this is a big if IMO) we’re at the point of specifying inference behavior, in the absence of Final, a literal value should be inferred as the type of the literal, not as the more constrained literal type (so in this case int
)
If anyone thinks the alternative, what would be the purpose of it not being Final if it’s a constant (can only be that exact literal value)?
carljm (Carl Meyer) February 22, 2025, 6:06pm 5
I do not think we should attempt to specify inference as part of this issue. If we decide that bare ClassVar
should be allowed, then it should be specified simply as “type is inferred as some type to which the RHS, if any, is assignable.” (I think mypy’s current behavior of choosing Any
should thus also be allowable under this specification.)
We also should specify the behavior of bare ClassVar
with no RHS; both mypy and pyright currently agree that this is not an error and the inferred type is Any/Unknown.
Viicos (Victorien) February 24, 2025, 4:39pm 6
I went with what @carljm mentioned:
- Allow bare
ClassVar
to be used: if an assigned value is available, the type should be inferred as some type to which this value is assignable. If no assigned value is available, the type
should be inferred to an unknown static type (such asAny
).
PR:
Does this require an approval from the typing council?
Jelle (Jelle Zijlstra) February 24, 2025, 4:57pm 7
I suppose type checkers should treat a bare ClassVar similar to an unannotated global. Since we already have that concept, it makes sense to me to allow bare ClassVar with the expectation that type checkers will use similar inference.
Yes.
Avasam (Avasam) February 24, 2025, 5:51pm 8
Glad I found this post. I wanted to ask about this exactly.
In real-world scenario, my use-case is to explicitly mark ClassVars as such on vars where the inferred type was already correct, but not marked as a ClassVariable.
I’ve been doing this a lot in distutils/setuptools. For example:
class Distribution:
common_usage = """\
Common commands: (see '--help-commands' for more)
setup.py build will build the package underneath 'build/'
setup.py install will install the package
"""
display_option_names = [translate_longopt(x[0]) for x in display_options]
I’d like to be able to do:
class Distribution:
common_usage: ClassVar = """\
Common commands: (see '--help-commands' for more)
setup.py build will build the package underneath 'build/'
setup.py install will install the package
"""
display_option_names: ClassVar = [translate_longopt(x[0]) for x in display_options]
Instead of:
class Distribution:
common_usage: ClassVar[str] = """\
Common commands: (see '--help-commands' for more)
setup.py build will build the package underneath 'build/'
setup.py install will install the package
"""
display_option_names: ClassVar[list[str]] = [translate_longopt(x[0]) for x in display_options]
If the infered type was the literal (like Final
does), then for me that’d defeat the purpose of wanting implicit annotation.
Also if your classvar is only allowed to be a specific Literal, shouldn’t you use Final
? Either way a subclass changing the value would be in violation.
jorenham (Joren Hammudoglu) March 1, 2025, 5:15pm 9
In the basedpyright there’s the reportUnannotatedClassAttribute
rule, which only allows bare ClassVar
if the class is @final
.
So
class A:
value: ClassVar = 1
reports
Type annotation for attribute `value` is required because this class is not decorated with `@final` (reportUnannotatedClassAttribute)
and
@final
class B:
value: ClassVar = 1
is accepted.
Viicos (Victorien) March 17, 2025, 7:56pm 10
erictraut (Eric Traut) April 9, 2025, 4:56pm 11
This proposal has been accepted by the TC and will be incorporated into the typing spec.
Thanks @Victorien for driving the proposal and to everyone else who contributed.