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

Python 개선 제안 한국어 번역

PEP 705 – TypedDict: 읽기 전용 항목

Author:
Alice Purcell <alicederyn at gmail.com>
Sponsor:
Pablo Galindo Salgado <pablogsal at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
07-Nov-2022
Python-Version:
3.13
Post-History:
30-Sep-2022, 02-Nov-2022, 14-Mar-2023, 17-Oct-2023, 04-Nov-2023
Resolution:
29-Feb-2024

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document: see readonly and typing.ReadOnly for up-to-date specs and documentation. Canonical typing specs are maintained at the typing specs site; runtime typing behaviour is described in the CPython documentation.

×

See the typing specification update process for how to propose changes to the typing spec.

초록

PEP 589는 고정된 키 집합을 갖는 딕셔너리를 위한 구조적 타입 TypedDict을 정의합니다. TypedDict는 변경 가능한 타입이므로, 유효한 입력을 허용하지 못하게 만들지 않으면서 읽기 전용 매개변수를 받는 메서드에 올바르게 어노테이션을 지정하기가 어렵습니다.

이 PEP는 이러한 사용을 지원하기 위해 새로운 타입 한정자인 typing.ReadOnly를 제안합니다. Python 문법은 변경하지 않습니다. TypedDict의 읽기 전용 키를 올바르게 사용하는 것은 정적 타입 검사기만 적용하도록 하며, 런타임에 Python 자체가 적용하지는 않습니다.

동기

문자열 키를 사용하는 (중첩될 수도 있는) 딕셔너리로 구조화된 데이터를 표현하는 것은 Python 프로그램에서 흔한 패턴입니다. PEP 589는 정확한 타입을 사전에 알고 있을 때 이러한 값의 타입 검사를 허용하지만, 더 구체적인 변형을 허용하는 읽기 전용 코드를 작성하기는 어렵습니다. 예를 들어 값이 서브타입일 수 있거나 가능한 타입들의 유니온을 제한하는 경우가 그렇습니다. 이는 광범위한 입력 구조를 지원할 수 있고 일반적으로 입력을 수정할 필요가 없는 서비스용 API를 작성할 때 특히 흔한 문제입니다.

순수 함수

movie_string 함수에 타입 힌트를 추가한다고 가정하십시오.:

def movie_string(movie: Movie) -> str:
    if movie.get("year") is None:
        return movie["name"]
    else:
        return f'{movie["name"]} ({movie["year"]})'

TypedDict를 사용하여 이 Movie 타입을 정의할 수 있습니다.:

from typing import NotRequired, TypedDict

class Movie(TypedDict):
    name: str
    year: NotRequired[int | None]

하지만 year가 필수인 다른 타입이 있다고 가정하십시오.:

class MovieRecord(TypedDict):
    name: str
    year: int

MovieRecordmovie_string에 전달하려고 하면 다음 오류가 발생합니다(mypy 사용):

Argument 1 to "movie_string" has incompatible type "MovieRecord"; expected "Movie"

이 특정 사용 사례는 타입 안전해야 하지만, 일반적인 경우 타입 검사기는 사용자가 MovieRecordMovie 매개변수에 전달하지 못하도록 올바르게 막습니다. Movie 클래스에는 MovieRecord의 타입 제약을 함수가 위반하게 만들 수 있는 변경 메서드가 있기 때문입니다(예: movie["year"] = None 또는 del movie["year"]). Movie에 변경 메서드가 없으면 이 문제가 사라집니다. 이는 PEP 544 Protocol을 사용하여 불변 인터페이스를 정의하면 달성할 수 있습니다.:

from typing import Literal, Protocol, overload

class Movie(Protocol):
    @overload
    def get(self, key: Literal["name"]) -> str: ...

    @overload
    def get(self, key: Literal["year"]) -> int | None: ...

    @overload
    def __getitem__(self, key: Literal["name"]) -> str: ...

    @overload
    def __getitem__(self, key: Literal["year"]) -> int | None: ...

이는 매우 반복적이고 실수하기 쉬우며, __contains__()keys() 와 같은 중요한 메서드 정의도 여전히 빠져 있습니다.

중첩된 딕셔너리 업데이트

TypedDict의 구조적 타이핑은 수정하는 항목의 타입만 제한하는 업데이트 함수를 작성할 수 있도록 해야 합니다.:

class HasTimestamp(TypedDict):
    timestamp: float

class Logs(TypedDict):
    timestamp: float
    loglines: list[str]

def update_timestamp(d: HasTimestamp) -> None:
    d["timestamp"] = now()

def add_logline(logs: Logs, logline: str) -> None:
    logs["loglines"].append(logline)
    update_timestamp(logs)  # Accepted by type checker

그러나 딕셔너리를 중첩하기 시작하면 더 이상 작동하지 않습니다.:

class HasTimestampedMetadata(TypedDict):
    metadata: HasTimestamp

class UserAudit(TypedDict):
    name: str
    metadata: Logs

def update_metadata_timestamp(d: HasTimestampedMetadata) -> None:
    d["metadata"]["timestamp"] = now()

def rename_user(d: UserAudit, name: str) -> None:
    d["name"] = name
    update_metadata_timestamp(d)  # Type check error: "metadata" is not of type HasTimestamp

이는 오류처럼 보이지만, HasTimestampedMetadata 인스턴스가 보유한 metadata 항목을 더 이상 Logs 인스턴스가 아닐 수도 있는 다른 HasTimestamp 인스턴스로 덮어쓸 수 있는 (바람직하지 않은) 능력 때문일 뿐입니다.

제네릭을 사용하여 이 문제를 우회할 수는 있지만(Python 3.11부터), 중첩된 각 딕셔너리마다 타입 매개변수가 필요하므로 매우 복잡합니다.

근거

TypedDict의 하나 이상의 항목을 업데이트할 수 있는 능력을 제거하면 이러한 문제를 해결할 수 있습니다. 이것은 해당 항목이 변경 불가능하다는 뜻은 아닙니다. 기반 딕셔너리에 대한 참조가 서로 다르지만 호환되는 타입으로 여전히 존재할 수 있으며, 그 타입에서는 해당 항목에 변경 작업을 수행할 수 있습니다. 이러한 항목은 “읽기 전용”이며, 이를 위해 새로운 typing.ReadOnly 타입 한정자를 도입합니다.

첫 번째 동기 부여 예제의 movie_string 함수는 다음과 같이 타입을 지정할 수 있습니다.:

from typing import NotRequired, ReadOnly, TypedDict

class Movie(TypedDict):
    name: ReadOnly[str]
    year: ReadOnly[NotRequired[int | None]]

def movie_string(movie: Movie) -> str:
    if movie.get("year") is None:
        return movie["name"]
    else:
        return f'{movie["name"]} ({movie["year"]})'

읽기 전용 항목과 읽기 전용이 아닌 항목을 혼합할 수 있으므로, 두 번째 동기 부여 예제에 올바르게 어노테이션을 지정할 수 있습니다.:

class HasTimestamp(TypedDict):
    timestamp: float

class HasTimestampedMetadata(TypedDict):
    metadata: ReadOnly[HasTimestamp]

def update_metadata_timestamp(d: HasTimestampedMetadata) -> None:
    d["metadata"]["timestamp"] = now()

class Logs(HasTimestamp):
    loglines: list[str]

class UserAudit(TypedDict):
    name: str
    metadata: Logs

def rename_user(d: UserAudit, name: str) -> None:
    d["name"] = name
    update_metadata_timestamp(d)  # Now OK

이러한 이점에 더해, 함수의 인자를 읽기 전용으로 표시하면(읽기 전용 항목이 있는 TypedDict와 같은 Movie를 사용하여) 해당 함수가 입력을 수정하지 않을 것임을 타입 검사기뿐 아니라 사용자에게도 명시적으로 알릴 수 있습니다. 이는 일반적으로 함수 인터페이스에 바람직한 속성입니다.

이 PEP는 ReadOnlyTypedDict에서만 유효하도록 만들 것을 제안합니다. 향후 가능한 확장으로는 프로토콜과 같은 추가 컨텍스트에서 이를 지원하는 것이 있습니다.

사양

새로운 typing.ReadOnly 타입 한정자가 추가됩니다.

typing.ReadOnly 타입 한정자

typing.ReadOnly 타입 한정자는 TypedDict 정의에서 선언된 항목을 변경할 수 없음(추가, 수정 또는 제거)을 나타내는 데 사용됩니다.:

from typing import ReadOnly

class Band(TypedDict):
    name: str
    members: ReadOnly[list[str]]

blur: Band = {"name": "blur", "members": []}
blur["name"] = "Blur"  # OK: "name" is not read-only
blur["members"] = ["Damon Albarn"]  # Type check error: "members" is read-only
blur["members"].append("Damon Albarn")  # OK: list is mutable

대안 함수 구문

TypedDict의 대안 함수 구문도 새로운 타입 한정자를 지원합니다.:

Band = TypedDict("Band", {"name": str, "members": ReadOnly[list[str]]})

다른 특수 타입과의 상호 작용

ReadOnly[]는 중첩 순서와 관계없이 Required[], NotRequired[]Annotated[]와 함께 사용할 수 있습니다.

class Movie(TypedDict):
    title: ReadOnly[Required[str]]  # OK
    year: ReadOnly[NotRequired[Annotated[int, ValueRange(-9999, 9999)]]]  # OK
class Movie(TypedDict):
    title: Required[ReadOnly[str]]  # OK
    year: Annotated[NotRequired[ReadOnly[int]], ValueRange(-9999, 9999)]  # OK

이는 PEP 655에서 도입된 동작과 일관됩니다.

상속

서브클래스는 읽기 전용 항목을 읽기 전용이 아닌 항목으로 재선언하여 변경할 수 있도록 할 수 있습니다.:

class NamedDict(TypedDict):
    name: ReadOnly[str]

class Album(NamedDict):
    name: str
    year: int

album: Album = { "name": "Flood", "year": 1990 }
album["year"] = 1973
album["name"] = "Dark Side Of The Moon"  # OK: "name" is not read-only in Album

읽기 전용 항목을 재선언하지 않으면 계속 읽기 전용으로 유지됩니다.:

class Album(NamedDict):
    year: int

album: Album = { "name": "Flood", "year": 1990 }
album["name"] = "Dark Side Of The Moon"  # Type check error: "name" is read-only in Album

서브클래스는 읽기 전용 항목의 값 타입을 더 좁힐 수 있습니다.:

class AlbumCollection(TypedDict):
    albums: ReadOnly[Collection[Album]]

class RecordShop(AlbumCollection):
    name: str
    albums: ReadOnly[list[Album]]  # OK: "albums" is read-only in AlbumCollection

서브클래스는 슈퍼클래스에서는 필수가 아니지만 읽기 전용인 항목을 필수로 지정할 수 있습니다.:

class OptionalName(TypedDict):
    name: ReadOnly[NotRequired[str]]

class RequiredName(OptionalName):
    name: ReadOnly[Required[str]]

d: RequiredName = {}  # Type check error: "name" required

서브클래스는 이러한 규칙을 결합할 수 있습니다.:

class OptionalIdent(TypedDict):
    ident: ReadOnly[NotRequired[str | int]]

class User(OptionalIdent):
    ident: str  # Required, mutable, and not an int

이는 구조적 타이핑의 결과일 뿐이지만, 이제 동작이 PEP 589에 명시된 규칙과 달라졌으므로 여기에서 강조합니다.

타입 일관성

이 절은 PEP 589 에서 도입된 타입 일관성 규칙을 이 PEP의 새로운 기능까지 다루도록 갱신합니다. 특히, 새로운 기능을 사용하지 않는 모든 타입 쌍은 (이미 일관성이 있었던 경우에만) 이러한 새로운 규칙에 따라 일관성이 있습니다.

A라는 TypedDict 타입은 AB와 구조적으로 호환되는 경우 TypedDict B와 일관성이 있습니다. 다음 조건을 모두 충족하는 경우에만 그렇습니다.

  • B의 각 항목에 대해, B의 항목이 읽기 전용이고 필수가 아니며 최상위 값 타입(ReadOnly[NotRequired[object]])인 경우를 제외하면 A에는 해당 키가 있습니다.
  • B의 각 항목에 대해 A에 해당 키가 있다면, A의 해당 값 타입은 B의 값 타입과 일관성이 있습니다.
  • B의 읽기 전용이 아닌 각 항목에 대해, 해당 값 타입은 A의 해당 값 타입과 일관성이 있습니다.
  • B의 각 필수 키에 대해 A의 해당 키는 필수입니다.
  • B의 각 비필수 키에 대해, B에서 해당 항목이 읽기 전용이 아니라면 A의 해당 키는 필수가 아닙니다.

논의:

  • TypedDict에서 지정되지 않은 모든 항목은 암묵적으로 ReadOnly[NotRequired[object]] 값 타입을 가집니다.
  • 읽기 전용 항목은 변경할 수 없으므로 공변적으로 동작합니다. 이는 Sequence와 같은 컨테이너 타입과 유사하며, 불변적으로 동작하는 읽기 전용이 아닌 항목과는 다릅니다. 예시:
    class A(TypedDict):
        x: ReadOnly[int | None]
    
    class B(TypedDict):
        x: int
    
    def f(a: A) -> None:
        print(a["x"] or 0)
    
    b: B = {"x": 1}
    f(b)  # Accepted by type checker
    
  • 명시적인 키 'x'가 없는 TypedDict 타입 A는 필수가 아닌 키 'x'를 가진 TypedDict 타입 B와 일관되지 않습니다. 런타임에 키 'x'가 존재하면서 호환되지 않는 타입을 가질 수 있고, 구조적 서브타이핑으로 인해 이 타입이 A를 통해서는 보이지 않을 수 있기 때문입니다. 이 규칙의 유일한 예외는 B의 항목이 읽기 전용이고 값의 타입이 최상위 타입(object)인 경우입니다. 예를 들어:
    class A(TypedDict):
        x: int
    
    class B(TypedDict):
        x: int
        y: ReadOnly[NotRequired[object]]
    
    a: A = { "x": 1 }
    b: B = a  # Accepted by type checker
    

업데이트 메서드

기존 타입 검사 규칙에 더해, 타입 검사기는 읽기 전용 항목이 있는 TypedDict를 해당 키를 선언하는 다른 TypedDict로 업데이트할 때 오류를 보고해야 합니다.:

class A(TypedDict):
    x: ReadOnly[int]
    y: int

a1: A = { "x": 1, "y": 2 }
a2: A = { "x": 3, "y": 4 }
a1.update(a2)  # Type check error: "x" is read-only in A

선언된 값이 최하위 타입(Never)인 경우는 제외합니다.:

class B(TypedDict):
    x: NotRequired[typing.Never]
    y: ReadOnly[int]

def update_a(a: A, b: B) -> None:
    a.update(b)  # Accepted by type checker: "x" cannot be set on b

참고: 어떤 것도 Never 타입과 일치하지 않으므로, 해당 타입으로 어노테이션된 항목은 존재하지 않아야 합니다.

키워드 인자 타이핑

PEP 692에서는 Unpack을 도입하여 **kwargsTypedDict를 어노테이션할 수 있도록 했습니다. 이러한 방식으로 사용되는 TypedDict의 항목 중 하나 이상을 읽기 전용으로 표시해도 메서드의 타입 시그니처에는 영향을 주지 않습니다. 그러나 함수 본문에서 해당 항목이 실제로 수정되는 것을 방지합니다.:

class Args(TypedDict):
    key1: int
    key2: str

class ReadOnlyArgs(TypedDict):
    key1: ReadOnly[int]
    key2: ReadOnly[str]

class Function(Protocol):
    def __call__(self, **kwargs: Unpack[Args]) -> None: ...

def impl(**kwargs: Unpack[ReadOnlyArgs]) -> None:
    kwargs["key1"] = 3  # Type check error: key1 is readonly

fn: Function = impl  # Accepted by type checker: function signatures are identical

런타임 동작

TypedDict타입에는 __readonly_keys____mutable_keys__라는 두 개의 새 속성이 추가되며, 각각 모든 읽기 전용 키와 읽기 전용이 아닌 키를 포함하는 frozenset이 됩니다.:

class Example(TypedDict):
    a: int
    b: ReadOnly[int]
    c: int
    d: ReadOnly[int]

assert Example.__readonly_keys__ == frozenset({'b', 'd'})
assert Example.__mutable_keys__ == frozenset({'a', 'c'})

typing.get_type_hintsinclude_extrasTrue가 아닌 한 모든 ReadOnly타입 한정자를 제거합니다.:

assert get_type_hints(Example)['b'] == int
assert get_type_hints(Example, include_extras=True)['b'] == ReadOnly[int]

typing.get_origintyping.get_argsReadOnly를 인식하도록 업데이트됩니다.:

assert get_origin(ReadOnly[int]) is ReadOnly
assert get_args(ReadOnly[int]) == (int,)

하위 호환성

이 PEP는 TypedDict에 새로운 기능을 추가하므로, TypedDict타입을 검사하는 코드는 이를 사용하는 타입을 지원하도록 변경해야 합니다. 이는 주로 타입 검사기에 영향을 줄 것으로 예상됩니다.

보안 관련 영향

이 PEP로 인해 발생하는 알려진 보안상 결과는 없습니다.

이 내용을 가르치는 방법

현재 관행에 맞춘 typing 모듈 문서의 변경 제안:

  • 이 PEP를 나열된 다른 PEP에 추가하십시오.
  • typing.ReadOnly를 TypedDict 및 이 PEP에 연결하여 추가하십시오.
  • TypedDict 항목에 다음 텍스트를 추가하십시오.

ReadOnly타입 한정자는 TypedDict정의에서 선언된 항목을 읽을 수는 있지만 변경할 수는 없음(추가, 수정 또는 제거)을 나타냅니다. 이는 값의 정확한 타입이 아직 알려지지 않았을 때 유용하며, 값을 수정하면 구조적 서브타입이 깨지기 때문입니다. insert example

참조 구현

pyright 1.1.333 fully implements this proposal.

거부된 대안

TypedMapping 프로토콜 타입

이 PEP의 이전 버전에서는 런타임 타입이 dict이어야 한다는 제약 없이 읽기 전용 TypedDict와 매우 유사하게 동작하는 TypedMapping 프로토콜 타입을 제안했습니다. 그러면 TypedMapping을 상속하여 TypedDict를 정의함으로써 이 PEP의 현재 버전에서 설명하는 동작을 얻을 수 있습니다. 그러나 추가적인 복잡성을 정당화할 만큼 강력한 사용 사례가 없어, 일단 더 복잡한 방안으로 보류했습니다.

고차 ReadOnly 타입

매개변수에서 변경 메서드를 제거하는 일반화된 고차 타입을 추가할 수 있습니다. 예를 들어 ReadOnly[MovieRecord]와 같습니다. TypedDict의 경우 이는 슈퍼클래스에서 선언된 항목을 포함하여 모든 항목에 ReadOnly를 추가하는 것과 같습니다. 이는 자연스럽게 TypedDict 서브클래스뿐 아니라 더 폭넓은 타입 집합에 대해 정의하고자 하게 만들며, 중첩 타입에 적용되는지와 어떻게 적용되는지에 관한 문제도 제기합니다. 이에 따라 이 PEP의 범위를 더 좁게 유지하기로 결정했습니다.

타입을 Readonly라고 부르기

Read-only는 일반적으로 하이픈으로 연결하며, CamelCase로 변환할 때 대시로 구분된 단어의 첫 글자를 대문자로 쓰는 것이 일반적인 관례인 것으로 보입니다. 이는 Wikipedia의 CamelCase 정의와 일치하는 것으로 보입니다. CamelCase는 각 단어의 첫 글자를 대문자로 씁니다. 그렇기는 하지만, 가능하다면 Python 핵심 라이브러리에서 가져온 Python 예시나 반례, 또는 더 나아가 이 관례에 대한 명확한 지침을 제공해 주시면 대단히 감사하겠습니다.

Final 어노테이션 재사용

관련 Final어노테이션은 제안된 ReadOnly 한정자가 TypedDict 항목의 수정을 방지하는 것처럼 속성이 수정되지 않도록 합니다. 그러나 서브클래스에서 재정의하는 것도 방지한다고 문서화되어 있습니다. PEP 591에서 인용하면 다음과 같습니다.

typing.Final 타입 한정자는 변수나 속성을 재할당하거나 재정의하거나 오버라이드해서는 안 된다는 것을 나타내는 데 사용합니다.

이는 ReadOnly의 의도된 용도에 부합하지 않습니다. Final이 컨텍스트에 따라 다르게 동작하도록 하여 혼란을 일으키기보다는 새로운 한정자를 도입하기로 했습니다.

readonly 플래그

이 PEP의 이전 버전에서는 TypedDict의 모든 항목이 읽기 전용이 되도록 보장하는 불리언 플래그를 도입했습니다.:

class Movie(TypedDict, readonly=True):
    name: str
    year: NotRequired[int | None]

movie: Movie = { "name": "A Clockwork Orange" }
movie["year"] = 1971  # Type check error: "year" is read-only

그러나 상속이 도입되면서 혼란이 발생했습니다.:

class A(TypedDict):
    key1: int

class B(A, TypedDict, readonly=True):
    key2: int

b: B = { "key1": 1, "key2": 2 }
b["key1"] = 4  # Accepted by type checker: "key1" is not read-only

frozen(dataclasses에서 유래한)에 익숙한 사람이라면 B의 정의만 보고 전체 타입이 읽기 전용이라고 가정하는 것이 합리적일 수 있습니다. 반면 total에 익숙한 사람이라면 읽기 전용이 현재 타입에만 적용된다고 가정하는 것이 합리적일 수 있습니다.

원래 제안에서는 이 방식으로 B를 정의하는 것을 타입 검사에서 오류로 처리하고 런타임 오류도 발생시키도록 하여 이러한 모호성을 없애려고 했습니다. 그러나 이는 total처럼 동작할 것으로 예상한 사람들에게 여전히 의외의 동작이었습니다.

readonly 플래그로 표현할 수 있는 추가 타입이 없었으므로, 모호함과 의외의 동작을 피하기 위해 제안에서 제거했습니다.

복사 및 기타 메서드를 통한 읽기 전용 한정자의 타입 검사된 제거 지원

이 PEP의 이전 버전에서는 다음과 같은 코드를 타입 검사기에서 지원하도록 요구했습니다.:

class A(TypedDict):
    x: ReadOnly[int]

class B(TypedDict):
    x: ReadOnly[str]

class C(TypedDict):
    x: int | str

def copy_and_modify(a: A) -> C:
    c: C = copy.copy(a)
    if not c['x']:
        c['x'] = "N/A"
    return c

def merge_and_modify(a: A, b: B) -> C:
    c: C = a | b
    if not c['x']:
        c['x'] = "N/A"
    return c

그러나 현재 typeshed에서는 이를 표현할 방법이 없으므로, 타입 검사기는 이러한 함수를 특수 처리해야 합니다. mypy와 pyright가 이미 지원하는 이러한 연산을 작성하는 방법이 있지만, 논란의 여지 없이 이 방법은 가독성이 떨어집니다.:

copied: C = { **a }
merged: C = { **a, **b }

이상적인 만큼 유연하지는 않지만, 현재 typeshed 스텁은 건전하며, 이 PEP가 승인되더라도 계속 건전합니다. typeshed를 업데이트하려면 두 개 이상의 딕셔너리를 병합한 결과로 생기는 타입을 표현하기 위한 타입 생성자와, 반환된 값이 공유되지 않음을 나타내는 타입 한정자(따라서 읽기 전용 및 제너릭의 불변성과 같은 타입 제약을 특정 방식으로 완화할 수 있음) 같은 새로운 타이핑 기능과, 타입 검사기가 이러한 기능을 어떻게 해석해야 하는지에 대한 세부 사항이 필요합니다. 이러한 기능은 언어에 추가할 가치가 있을 수 있지만, 이 PEP의 범위를 벗어납니다.

따라서 typeshed 스텁의 업데이트는 모두 미루었습니다.

TypedDict에서 지정되지 않은 키 방지

다음과 같은 “타입 판별” 코드를 살펴보십시오.:

class A(TypedDict):
  foo: int

class B(TypedDict):
  bar: int

def get_field(d: A | B) -> int:
  if "foo" in d:
    return d["foo"]  # !!!
  else:
    return d["bar"]

이는 흔한 관용구이며, Typescript와 같은 다른 언어에서도 이를 허용합니다. 그러나 기술적으로 이 코드는 건전하지 않습니다. Bfoo를 선언하지 않지만, B의 인스턴스에는 해당 키가 존재할 수 있으며, 연결된 값은 어떤 타입이든 될 수 있습니다.:

class C(TypedDict):
  foo: str
  bar: int

c: C = { "foo": "hi", "bar" 3 }
b: B = c  # OK: C is structurally compatible with B
v = get_field(b)  # Returns a string at runtime, not an int!

mypy는 표시된 줄에서 get_field의 정의를 TypedDict "B" has no key "foo"라는 오류와 함께 거부합니다. 이는 상당히 혼란스러운 오류 메시지이지만, 이러한 비건전성 때문에 발생합니다.

이를 수정하는 한 가지 방법은 Bfoo가 포함되지 않도록 명시적으로 방지하는 것입니다.:

class B(TypedDict):
  foo: NotRequired[Never]
  bar: int

b: B = c  # Type check error: key "foo" not allowed in B

그러나 이를 위해서는 판별에 사용될 수 있는 모든 가능한 키를 모든 타입에 명시적으로 선언해야 하므로, 일반적으로 실현 가능하지 않습니다. 더 나은 방법은 지정되지 않은 모든 키가 B에 포함되지 않도록 하는 수단을 마련하는 것입니다. mypy는 PEP 591@final 데코레이터를 사용하여 이를 지원합니다.:

@final
class B(TypedDict):
  bar: int

여기서의 논리는 이를 통해 C나 다른 어떤 타입도 B의 “하위 클래스”로 간주되지 않도록 하므로, 명시적으로 bottom 타입이라고 선언되지 않았더라도 B의 인스턴스에는 foo키가 절대 포함되지 않는다고 신뢰할 수 있다는 것입니다.

그러나 읽기 전용 항목이 도입되면, 이러한 논리에 따라 타입 검사기는 다음을 금지해야 합니다.:

@final
class D(TypedDict):
  field: ReadOnly[Collection[str]]

@final
class E(TypedDict):
  field: list[str]

e: E = { "field": ["value1", "value2"] }
d: D = e  # Error?

여기서의 개념적 문제는 TypedDict가 구조적 타입이라는 점입니다. TypedDict는 실제로 서브클래스화할 수 없습니다. 따라서 TypedDict에 @final을 사용하는 것은 잘 정의되지 않으며, PEP 591에도 분명히 언급되어 있지 않습니다.

이 PEP의 이전 버전에서는 다른 종류의 구조적 호환성은 막지 않으면서 다른 키가 사용되는 것을 명시적으로 방지하는 새 플래그를 TypedDict에 추가하여 이 문제를 해결하자고 제안했습니다.:

class B(TypedDict, other_keys=Never):
  bar: int

b: B = c  # Type check error: key "foo" not allowed in B

그러나 초안을 작성하는 과정에서 상황이 바뀌었습니다.

따라서 이 PEP에서 이 문제를 다뤄야 할 긴급성이 줄어들었으며, 이 문제는 PEP-728로 미루어졌습니다.