Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 767 – Annotating Read-Only Attributes

PEP 767 – Annotating Read-Only Attributes

Author:
Łukasz Modzelewski
Sponsor:
Carl Meyer <carl at oddbird.net>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Topic:
Typing
Created:
18-Nov-2024
Python-Version:
3.15
Post-History:
09-Oct-2024 05-Dec-2024

Table of Contents

Abstract

PEP 705 introduced the typing.ReadOnly type qualifier to allow defining read-only typing.TypedDict items.

This PEP proposes using ReadOnly in annotations of class and protocol attributes, as a single concise way to mark them read-only.

Akin to PEP 705, it makes no changes to setting attributes at runtime. Correct usage of read-only attributes is intended to be enforced only by static type checkers.

Terminology

This PEP uses “read-only” to describe attributes which may be read, but not assigned to (except in limited cases to support initialization) or deleted.

Motivation

The Python type system lacks a single concise way to mark an attribute read-only. This feature is present in other statically and gradually typed languages (such as C# or TypeScript), and is useful for removing the ability to assign to or delete an attribute at a type checker level, as well as defining a broad interface for structural subtyping.

Classes

Today, there are three major ways of achieving read-only attributes, honored by type checkers:

  • annotating the attribute with typing.Final:
    class Foo:
        number: Final[int]
    
        def __init__(self, number: int) -> None:
            self.number = number
    
    
    class Bar:
        def __init__(self, number: int) -> None:
            self.number: Final = number
    
    • Supported by dataclasses (and type checkers since typing#1669).
    • Overriding number is not possible - the specification of Final imposes that the name cannot be overridden in subclasses.
  • marking the attribute “_internal”, and exposing it via read-only @property:
    class Foo:
        _number: int
    
        def __init__(self, number: int) -> None:
            self._number = number
    
        @property
        def number(self) -> int:
            return self._number
    
    • Overriding number is possible, but limited to using @property. [1]
    • Read-only at runtime. [2]
    • Requires extra boilerplate.
    • Supported by dataclasses, but does not compose well - the synthesized __init__ and __repr__ will use _number as the parameter/attribute name.
  • using a “freezing” mechanism, such as dataclasses.dataclass() or typing.NamedTuple:
    @dataclass(frozen=True)
    class