Skip to content

Clarify ClassVar protocol conformance expectations - #2340

Merged
JelleZijlstra merged 2 commits into
mainfrom
alex/protocol-classvar-conformance
Aug 13, 2026
Merged

Clarify ClassVar protocol conformance expectations#2340
JelleZijlstra merged 2 commits into
mainfrom
alex/protocol-classvar-conformance

Conversation

@AlexWaygood

Copy link
Copy Markdown
Member

Summary

Relax two conformance-suite assertions that require an explicit ClassVar declaration when matching protocol members, even though the typing specification does not clearly impose that requirement. Add coverage for cases that must be rejected under either reasonable interpretation of the specification.

Motivation

astral-sh/ruff#27530 changed ty's protocol matching to satisfy two existing conformance-suite assertions. Before that change, ty treated ClassVar protocol members structurally: given a protocol member x: ClassVar[T], an implementing class needed to provide an attribute that was readable on instances and both readable and writable on the class object.

For example:

from typing import ClassVar, Protocol


class HasValue(Protocol):
    value: ClassVar[int]


class Implementation:
    value: int = 1


implementation: HasValue = Implementation()

Implementation.value = 2
print(Implementation.value)
print(Implementation().value)

Under a structural interpretation, Implementation provides everything the protocol requires: its value attribute is readable and writable on the class object and readable on instances. The fact that the implementing attribute permits additional instance-level operations does not prevent callers using the protocol type from respecting the protocol's restrictions.

After astral-sh/ruff#27530, ty rejects this implementation solely because Implementation.value was not explicitly declared with ClassVar. That makes protocol matching depend on how an attribute was nominally declared, rather than on the interface the implementing class actually exposes.

I would prefer to revert the nominal declaration check added in astral-sh/ruff#27530, since I feel that protocol subtyping and assignability should always be structural, never nominal. Unfortunately, doing so currently causes ty to fail conformance assertions that go beyond what the specification actually says.

What the specification requires

The protocol-member section says:

To distinguish between protocol class variables and protocol instance variables, the special ClassVar annotation should be used.

It doesn't, however, say anything about how a ClassVar qualifier on protocols should be interpreted. Other sections also do not go into this in much detail:

Two interpretations therefore seem possible:

  1. A ClassVar protocol member requires an implementing attribute to be explicitly declared with ClassVar.
  2. A ClassVar protocol member imposes only structural requirements: the attribute must be readable and writable on the class object and readable on instances.

My preference is the second interpretation because it preserves structural protocol matching.

Conformance-suite changes

Mark the existing disputed assertions in protocols_definition.py and protocols_class_objects.py with # E? rather than # E. This permits checkers to accept or reject implementations whose only disputed property is the absence of an explicit matching ClassVar declaration.

Also add required-error assertions for implementations that should (in my view) fail under either interpretation:

from typing import ClassVar, Protocol


class Proto(Protocol):
    value: ClassVar[int]


class InstanceOnly:
    def __init__(self) -> None:
        self.value: int = 42


class Meta(type):
    value: int = 42


class MetaclassOnly(metaclass=Meta):
    pass


instance_only: Proto = InstanceOnly()  # E: value is not readable on the class object
metaclass_only: Proto = MetaclassOnly()  # E: value is not readable on instances

InstanceOnly.value is available only on instances, so it cannot satisfy the class-object requirements. MetaclassOnly.value is available on the class object through its metaclass, but it is not available on instances of MetaclassOnly.

The first new assertion also exposes an existing pycroscope bug: pycroscope incorrectly accepts an instance-only attribute as satisfying a ClassVar protocol member. This PR therefore updates its recorded conformance result to Partial.

@AlexWaygood AlexWaygood added the topic: conformance tests Issues with the conformance test suite label Aug 12, 2026
@AlexWaygood
AlexWaygood marked this pull request as ready for review August 12, 2026 13:17
Base automatically changed from alex/update-conformance to main August 12, 2026 14:27
@carljm
carljm force-pushed the alex/protocol-classvar-conformance branch from b297a5f to 7315f15 Compare August 12, 2026 14:27
@carljm

carljm commented Aug 12, 2026

Copy link
Copy Markdown
Member

I agree that the current conformance suite is overstepping what the spec actually says, and the relaxed checks in this PR should not be errors. I would prefer to clarify the spec text and assert that those two lines should not be errors, rather than use E?, but we can see what other typing council members think.

@AlexWaygood
AlexWaygood force-pushed the alex/protocol-classvar-conformance branch from 7315f15 to 7ab5fee Compare August 12, 2026 14:35
@jorenham

jorenham commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

class attributes can (usually) also be accessed on self:

class A:
    a: ClassVar[int] = 1
    b: int = 1

    def get_a(self) -> int:
        return self.a  # valid

    def get_b(self) -> int:
        return self.b  # valid

So in that sense, a ClassVar[T] attribute is more general than a T attribute, because the former can be accessed from both the class and the instance, whereas the latter is only guaranteed to be accessible from the instance. That's why I agree that rejecting the first example is the right call.

I also agree with Carl that we should specify this in the SPEC first. Then, after we've agreed on semantics, we can adjust the conformance tests accordingly.

@AlexWaygood

AlexWaygood commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

class attributes can usually also be accessed on self:

yes, I say this in my PR description, and it's covered by the new assertions I added

So in that sense, a ClassVar[T] attribute is more general than a T attribute

I don't think this is fully true: a ClassVar is definitely readable and writable on the class object, which is not necessarily guaranteed with an instance attribute, but it is not writable on instances of the class, and that is guaranteed with a normal instance attribute

I also agree with Carl that we should specify this in the SPEC first. Then, after we've agreed on semantics, we can adjust the conformance tests accordingly.

I fully agree that we should have a conversation about what the spec should say! This, however, is a more involved process that requires discussion on DPO and consensus from the community more broadly. In the meantime, ty has behaviour that I'm unhappy with, and improving that behaviour is not possible without regressing our conformance score.

It's precisely because this is not specified at all right now that I wanted to relax the assertions and instead only assert the minimum behaviour that is compatible with all reasonable interpretations of the spec that I'm currently aware of. Completely removing the E: assertions from the suite would make more type checkers be marked as uncompliant, which feels unfair until we've had a discussion about exactly what the spec should say.

@jorenham

Copy link
Copy Markdown
Collaborator

I don't think this is fully true: a ClassVar is readable and writable on the class object, which is not guaranteed with an instance attribute, but it is not writable on instances of the class, and that is guaranteed with a normal instance attribute

Hmm yea that's a good point. It's too bad we have no good way of specifying whether the ClassVar of a Protocol is intended as read-only or not. Either way, it's indeed more complicated that I had hoped, which is all the more reason that we first write this down in the spec. But as you already noted, it will indeed likely take some time to get to that point.

So yea, maybe accepting these changes, as a temporary patch of sorts, is a good idea.

@JelleZijlstra

Copy link
Copy Markdown
Member

It's too bad we have no good way of specifying whether the ClassVar of a Protocol is intended as read-only or not.

This is PEP 767 which unfortunately stalled over some concerns unrelated to protocols.

@JelleZijlstra
JelleZijlstra merged commit bee91c2 into main Aug 13, 2026
7 checks passed
@JelleZijlstra
JelleZijlstra deleted the alex/protocol-classvar-conformance branch August 13, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic: conformance tests Issues with the conformance test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants