2025, Nov 02 15:00

Expressing a Read-Only Class Attribute in a Python Protocol with Enum, ClassVar and ReadOnly (PEP 767)

Learn how to model a read-only class variable in Python Protocols for Enum-backed constants using ClassVar and ReadOnly per PEP 767, with checker caveats.

When you model a family of classes that expose the same Enum-valued constant as a read-only class attribute, it’s natural to describe that contract with a Protocol. The stumbling block: how to express a read-only class variable in a Protocol so that classes with an immutable Enum constant are accepted, without turning it into an instance property or a classmethod.

Problem setup

Suppose we have an Enum and a couple of classes that pin a class-level constant to different Enum members. The attribute is immutable and should be treated as a class variable.

from typing import ClassVar, Protocol, Final, Literal
from enum import Enum

class RoleKind(Enum):
    ALPHA = 0
    BETA = 1
    
class EngineA:
    category: Final = RoleKind.ALPHA

class EngineB:
    category: Final = RoleKind.BETA

The goal is to define a Protocol that both EngineA and EngineB conform to, capturing that category is a class-level, read-only attribute whose type is RoleKind.

What goes wrong

Intuitively, one might try shaping the Protocol as if category were an instance property, or as if Final directly applied inside the Protocol. Variants like these don’t achieve the intended behavior:

class ContractA(Protocol):
    @property
    def category(self) -> RoleKind: ...

class ContractB(Protocol):
    category: Final[RoleKind]

Other angles, such as attempting to wrap the type with ClassVar behind a property or switching to a classmethod-like signature, are not allowed in this context and still miss the target of a read-only class variable:

class ContractC(Protocol):
    @property
    def category(self) -> ClassVar[RoleKind]: ...

class ContractD(Protocol):
    @property
    @classmethod
    def category(cls) -> RoleKind: ...

Even exploring a Generic with Literal does not solve the Protocol definition for this case.

The essence of the issue

The task is to express a class attribute in a Protocol that is conceptually read-only and holds a RoleKind, matching classes where the attribute is defined once at the class level with Final. Treating it as an instance property, a classmethod, or a regular instance attribute drifts away from that intent. What’s needed is a way to say “read-only class variable” directly within the Protocol’s type system.

Solution

The spelling that captures a read-only class variable inside a Protocol is:

class ContractSpec(Protocol):
    category: ClassVar[ReadOnly[RoleKind]]

This follows the semantics described by PEP 767 for class attributes. As noted in that specification, this form is currently in draft and is not supported by any type checkers.

Why this matters

Describing class-level constants precisely in Protocols strengthens API contracts and enables safer refactoring. It makes intent explicit: the attribute is class-scoped, not per-instance, and it’s immutable. When multiple implementations expose such an Enum-backed constant, unifying them under a single Protocol clarifies design and reduces ambiguity around attribute shape and mutability.

Takeaways

If you need to express a read-only class variable in a Protocol, the right form is category: ClassVar[ReadOnly[RoleKind]] in line with PEP 767’s semantics. While this is not available in type checkers yet, it is the direction that captures the exact intent for immutable class attributes in Protocol definitions. Until such support lands, be aware of the limitation and keep your class contracts consistent so that adopting the precise annotation will be straightforward when it becomes available.

The article is based on a question from StackOverflow by Durtal and an answer by InSync.