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:

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.


  1. 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:

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.