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

Python 개선 제안 한국어 번역

PEP 728 – 타입이 지정된 추가 항목을 갖는 TypedDict

Author:
Zixuan James Li <p359101898 at gmail.com>
Sponsor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
12-Sep-2023
Python-Version:
3.15
Post-History:
09-Feb-2024
Resolution:
15-Aug-2025

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at TypedDict.

×

See PEP 1 for how to propose changes.

초록

이 PEP는 TypedDict의 추가 항목에 타입을 지정할 수 있도록 closed와 extra_items라는 두 가지 클래스 매개변수를 추가합니다. 이를 통해 닫힌 TypedDict 타입을 정의하거나, 지정된 타입의 추가 항목을 허용하면서 dict에 나타날 수 있는 키 중 일부에 타입을 지정해야 하는 요구를 해결합니다.

동기

typing.TypedDict 타입은 딕셔너리에서 알려진 각 항목의 값 타입을 어노테이션할 수 있습니다. 그러나 structural 할당 가능성 때문에 TypedDict는 그 타입을 통해서는 보이지 않는 추가 항목을 가질 수 있습니다. 현재는 TypedDict 타입의 일관된 하위 타입에 존재할 수 있는 항목의 타입을 제한할 방법이 없습니다.

추가 항목을 명시적으로 금지하기

TypedDict의 현재 동작으로는 타입에 추가 항목이 없을 것으로 예상되는 경우에도 사용자가 그러한 TypedDict 타입을 정의할 수 없습니다.

추가 항목이 존재할 수 있기 때문에 타입 검사기는 TypedDict의 .items()와 .values()에 대해 더 정밀한 반환 타입을 추론할 수 없습니다. 이 문제는 닫힌 TypedDict 타입을 정의하면 해결할 수 있습니다.

또 다른 사용 사례로는 in 검사로 타입 좁히기를 가능하게 하는 건전한 방법을 들 수 있습니다:

class Movie(TypedDict):
    name: str
    director: str

class Book(TypedDict):
    name: str
    author: str

def fun(entry: Movie | Book) -> None:
    if "author" in entry:
        reveal_type(entry)  # Revealed type is still 'Movie | Book'

Movie에 할당 가능한 dict가 author 키를 갖는 것을 막는 장치는 없으며, 현재 명세에서는 타입 검사기가 그 타입을 narrow하는 것이 올바르지 않습니다.

특정 타입의 추가 항목 허용하기

가능한 키 중 일부만 알려진 API 인터페이스나 레거시 코드베이스를 지원하려면 특정 값 타입의 추가 항목을 명시적으로 지정할 수 있는 것이 유용할 것입니다.

그러나 타이핑 명세는 TypedDict의 생성을 검사할 때 더 제한적이어서 사용자가 이렇게 하지 못하도록 막습니다:

class MovieBase(TypedDict):
    name: str

def foo(movie: MovieBase) -> None:
    # movie can have extra items that are not visible through MovieBase
    ...

movie: MovieBase = {"name": "Blade Runner", "year": 1982}  # Not OK
foo({"name": "Blade Runner", "year": 1982})  # Not OK

TypedDict를 생성할 때는 이 제한이 적용되지만, structural 할당 가능성 때문에 TypedDict는 그 타입을 통해서는 보이지 않는 추가 항목을 가질 수 있습니다. 예를 들면 다음과 같습니다:

class Movie(MovieBase):
    year: int

movie: Movie = {"name": "Blade Runner", "year": 1982}
foo(movie)  # OK

추가 항목이 MovieBase의 어떤 일관된 하위 타입에서 비롯되어 존재할 수 있더라도, 타입 안전성을 깨뜨리지 않고서는 in 검사를 통해 추가 항목의 존재를 인정하고 이에 접근할 수 없습니다:

def bar(movie: MovieBase) -> None:
    if "year" in movie:
        reveal_type(movie["year"])  # Error: TypedDict 'MovieBase' has no key 'year'

추가 항목을 허용하기 위한 몇 가지 우회 방법이 이미 구현되어 있지만, 그중 어느 것도 이상적이지 않습니다. mypy의 경우, --disable-error-code=typeddict-unknown-key는 TypedDict의 알 수 없는 키에 한해 타입 검사 오류를 억제합니다. 이는 유연성을 위해 타입 안전성을 희생하며, TypedDict 타입이 특정 타입에 할당 가능한 값 타입을 갖는 추가 키를 기대한다는 것을 지정하는 방법도 제공하지 않습니다.

Unpack에 대한 추가 키 지원

PEP 692는 TypedDict를 Unpack과 함께 사용하여 **kwargs로 표현되는 개별 키워드 인자의 타입을 정밀하게 어노테이션하는 방법을 추가합니다. 그러나 TypedDict는 임의의 추가 항목을 받아들이도록 정의할 수 없기 때문에, TypedDict를 정의하는 시점에 알려지지 않은 추가 키워드 인자를 허용할 수 없습니다.

기존 코드베이스에서 **kwargs에 PEP 692 이전 방식의 타입 어노테이션을 사용하고 있음을 고려하면, 이전의 타이핑 동작을 Unpack과 함께 지원할 수 있도록 TypedDict에서 추가 항목을 받아들이고 타입을 지정하는 것은 가치가 있을 것입니다.

이전 논의

이 PEP에서 도입하는 새로운 기능은 타입 시스템에서 오랫동안 제기되어 온 여러 기능 요청을 해결할 것입니다. 이전 논의는 다음과 같습니다.

  • “final TypedDict”를 요청한 Mypy 이슈 (2019). 이 논의는 @final 데코레이터에 초점을 맞추고 있지만, 근본적인 기능 요청은 이 PEP로 해결될 것입니다.
  • TypedDict에 임의의 추가 키가 포함될 수 있음을 표현하는 방법을 요청한 메일링 리스트 스레드 (2020).
  • PEP 692에서 도입된 Unpack 메커니즘의 확장에 관한 논의 (2023).
  • PEP 705의 이전 초안에서 유사한 기능을 제안했으나(2023), 해당 PEP를 더 단순하게 유지하기 위해 제거되었습니다.
  • “정확한(exact)” TypedDict에 관한 논의 (2024).

근거

TypedDict에 str 타입의 추가 항목을 허용하는 타입이 필요하다고 가정하겠습니다.

TypeScript의 인덱스 시그니처는 이를 허용합니다:

type Foo = {
    a: string
    [key: string]: string
}

이 제안은 문법 변경 없이 유사한 기능을 지원하여 기존 할당 가능성 규칙을 자연스럽게 확장하는 것을 목표로 합니다.

TypedDict에 extra_items 클래스 매개변수를 추가할 것을 제안합니다. 이 매개변수는 type expression을 인자로 받습니다. 이 매개변수가 있으면 추가 항목이 허용되며, 추가 항목의 값 타입은 해당 타입 표현식의 값에 할당 가능해야 합니다.

이를 응용하면 추가 항목을 금지할 수 있습니다. 리터럴 True 또는 False만 인자로 받는 closed 클래스 매개변수를 추가할 것을 제안합니다. closed와 extra_items를 동시에 사용하면 런타임 오류가 발생해야 합니다.

인덱스 시그니처와 달리, 알려진 항목의 타입은 extra_items 인자에 할당 가능할 필요가 없습니다.

이 접근 방식에는 몇 가지 장점이 있습니다:

  • 타이핑 명세에 정의된 할당 가능성 규칙을 기반으로 삼을 수 있으며, 이 규칙에서 extra_items는 의사 항목(pseudo-item)으로 취급될 수 있습니다.
  • 추가 항목의 타입을 지정하기 위해 문법 변경을 도입할 필요가 없습니다.
  • 알려진 항목의 값 타입이 extra_items에 assignable하도록 요구하지 않고도 추가 항목의 타입을 정밀하게 지정할 수 있습니다.
  • extra_items와 closed는 모두 명시적으로 선택해야만 적용되는 기능이므로 하위 호환성을 잃지 않습니다.

명세

이 명세는 원래 TypedDict 명세에 대한 변경 사항을 강조하기 위해 PEP 589와 대응되는 구조로 작성되었습니다.

extra_items가 지정되면, 추가 항목은 extra_items 인자와 일치하는 비필수 항목으로 취급되며, 지원되는 연산과 지원되지 않는 연산을 판단할 때 그 키가 허용됩니다.

extra_items 클래스 매개변수

기본적으로 extra_items는 설정되어 있지 않습니다. extra_items를 지정하는 TypedDict 타입의 경우, 생성 시 알 수 없는 각 항목의 값 타입은 비필수이며 extra_items 인자에 할당 가능해야 합니다. 예를 들면 다음과 같습니다:

class Movie(TypedDict, extra_items=bool):
    name: str

a: Movie = {"name": "Blade Runner", "novel_adaptation": True}  # OK
b: Movie = {
    "name": "Blade Runner",
    "year": 1982,  # Not OK. 'int' is not assignable to 'bool'
}

여기서 extra_items=bool은 'name' 이외의 항목이 bool 값 타입을 가지며 비필수임을 지정합니다.

대안적인 인라인 문법도 지원됩니다:

Movie = TypedDict("Movie", {"name": str}, extra_items=bool)

추가 항목에 접근하는 것은 허용됩니다. 타입 검사기는 extra_items 인자로부터 추가 항목의 값 타입을 추론해야 합니다:

def f(movie: Movie) -> None:
    reveal_type(movie["name"])              # Revealed type is 'str'
    reveal_type(movie["novel_adaptation"])  # Revealed type is 'bool'

extra_items는 서브클래싱을 통해 상속됩니다:

class MovieBase(TypedDict, extra_items=ReadOnly[int | None]):
    name: str

class Movie(MovieBase):
    year: int

a: Movie = {"name": "Blade Runner", "year": None}  # Not OK. 'None' is incompatible with 'int'
b: Movie = {
    "name": "Blade Runner",
    "year": 1982,
    "other_extra_key": None,
}  # OK

여기서 a의 'year'는 Movie에 정의된 추가 키이며, 그 값 타입은 int입니다. b의 'other_extra_key'는 또 다른 추가 키이며, 그 값 타입은 MovieBase에 정의된 extra_items의 값에 할당 가능해야 합니다.

closed 클래스 매개변수

extra_items와 closed=True 중 어느 것도 지정하지 않으면 closed=False로 간주합니다. 기본 TypedDict 동작을 유지하기 위해, TypedDict는 상속이나 할당 가능성 검사 시 값 타입이 ReadOnly[object]인 필수가 아닌 추가 항목을 허용해야 합니다. TypedDict 객체 생성에 포함된 추가 키는 TypedDict의 타이핑 명세에서 언급한 대로 여전히 검출해야 합니다.

closed=True를 설정하면 추가 항목이 허용되지 않습니다. Never에 할당 가능한 값 타입은 존재할 수 없으므로, 이는 extra_items=Never와 동등합니다. 같은 TypedDict 정의에서 closed와 extra_items 매개변수를 함께 사용하면 런타임 오류입니다.

total과 마찬가지로, closed 인자의 값으로는 리터럴 True 또는 False만 지원됩니다. 타입 검사기는 리터럴이 아닌 값을 모두 거부해야 합니다.

closed=False를 전달하면 임의의 다른 키가 존재할 수 있고 서브클래스가 임의의 항목을 추가할 수 있는 기본 TypedDict 동작을 명시적으로 요청하게 됩니다. 상위 클래스가 closed=True를 가지거나 extra_items를 설정한 경우 closed=False를 전달하면 타입 검사기 오류입니다.

closed를 제공하지 않으면 그 동작은 상위 클래스에서 상속됩니다. 상위 클래스가 TypedDict 자체이거나 상위 클래스가 closed=True나 extra_items 매개변수를 가지지 않는 경우, 이전의 TypedDict 동작이 유지되어 임의의 추가 항목이 허용됩니다. 상위 클래스가 closed=True를 가지면 자식 클래스도 닫힌 상태가 됩니다:

class BaseMovie(TypedDict, closed=True):
    name: str

class MovieA(BaseMovie):  # OK, still closed
    pass

class MovieB(BaseMovie, closed=True):  # OK, but redundant
    pass

class MovieC(BaseMovie, closed=False):  # Type checker error
    pass

closed=True가 extra_items=Never와 동등하므로, extra_items=Never에 적용되는 규칙이 closed=True에도 똑같이 적용됩니다. 둘 다 같은 효과를 내지만, extra_items=Never보다 closed=True를 사용하는 것이 권장됩니다.

extra_items 인자가 읽기 전용 타입이면 서브클래싱할 때 closed=True를 사용할 수 있습니다:

class Movie(TypedDict, extra_items=ReadOnly[str]):
    pass

class MovieClosed(Movie, closed=True):  # OK
    pass

class MovieNever(Movie, extra_items=Never):  # OK, but 'closed=True' is preferred
    pass

이에 대해서는 뒤의 절에서 더 자세히 다룹니다.

closed는 함수형 문법에서도 지원됩니다:

Movie = TypedDict("Movie", {"name": str}, closed=True)

전체성과의 상호 작용

extra_items에 Required[]나 NotRequired[]를 사용하면 오류입니다. total=False와 total=True는 extra_items 자체에는 영향을 주지 않습니다.

추가 항목은 TypedDict의 전체성과 관계없이 필수가 아닙니다. NotRequired 항목에 사용할 수 있는 연산은 추가 항목에도 사용할 수 있어야 합니다:

class Movie(TypedDict, extra_items=int):
    name: str

def f(movie: Movie) -> None:
    del movie["name"]  # Not OK. The value type of 'name' is 'Required[str]'
    del movie["year"]  # OK. The value type of 'year' is 'NotRequired[int]'

Unpack과의 상호 작용

타입 검사 목적상, 추가 항목이 있는 Unpack[SomeTypedDict]는 일반 매개변수에서의 동등한 형태로 취급해야 하며, 함수 매개변수에 대한 기존 규칙이 그대로 적용됩니다:

class MovieNoExtra(TypedDict):
    name: str

class MovieExtra(TypedDict, extra_items=int):
    name: str

def f(**kwargs: Unpack[MovieNoExtra]) -> None: ...
def g(**kwargs: Unpack[MovieExtra]) -> None: ...

# Should be equivalent to:
def f(*, name: str) -> None: ...
def g(*, name: str, **kwargs: int) -> None: ...

f(name="No Country for Old Men", year=2007) # Not OK. Unrecognized item
g(name="No Country for Old Men", year=2007) # OK

읽기 전용 항목과의 상호 작용

extra_items 인자가 ReadOnly[] type qualifier로 어노테이션되면, TypedDict의 추가 항목은 읽기 전용 항목의 속성을 가집니다. 이는 읽기 전용 항목에 명시된 상속 규칙과 상호 작용합니다.

특히, TypedDict 타입이 extra_items를 읽기 전용으로 지정하면, 해당 TypedDict 타입의 서브클래스는 extra_items를 재선언할 수 있습니다.

닫히지 않은 TypedDict 타입은 값 타입이 ReadOnly[object]인 필수가 아닌 추가 항목을 암묵적으로 허용하므로, 그 서브클래스는 extra_items 인자를 더 구체적인 타입으로 재정의할 수 있습니다.

자세한 내용은 뒤의 절에서 다룹니다.

상속

extra_items는 일반 key: value_type 항목과 비슷한 방식으로 상속됩니다. 다른 키와 마찬가지로 상속 규칙과 읽기 전용 항목 상속 규칙이 적용됩니다.

extra_items가 이 규칙들과 어떻게 상호 작용하는지 정의하려면 이 규칙들을 재해석해야 합니다.

  • 서브클래스에서 부모 TypedDict 클래스의 필드 타입을 변경하는 것은 허용되지 않습니다.

첫째, 슈퍼클래스에서 ReadOnly로 선언되지 않은 한 서브클래스에서 extra_items의 값을 변경하는 것은 허용되지 않습니다.:

class Parent(TypedDict, extra_items=int | None):
    pass

class Child(Parent, extra_items=int): # Not OK. Like any other TypedDict item, extra_items's type cannot be changed
    pass

둘째, extra_items=T는 사실상 TypedDict가 받아들이는 이름 없는 모든 항목의 값 타입을 정의하고, 해당 항목들을 필수가 아닌 항목으로 표시합니다. 따라서 위의 제한은 서브클래스에서 정의되는 모든 추가 항목에 적용됩니다. 서브클래스에서 추가되는 각 항목에 대해 다음 조건이 모두 적용되어야 합니다:

  • extra_items가 읽기 전용인 경우
    • 항목은 필수일 수도 있고 필수가 아닐 수도 있습니다
    • 항목의 값 타입은 T에 assignable합니다
  • extra_items가 읽기 전용이 아닌 경우
    • 항목은 필수가 아닙니다
    • 항목의 값 타입은 T와 consistent합니다
  • extra_items가 재정의되지 않으면 서브클래스는 이를 그대로 상속합니다.

예를 들면 다음과 같습니다:

class MovieBase(TypedDict, extra_items=int | None):
    name: str

class MovieRequiredYear(MovieBase):  # Not OK. Required key 'year' is not known to 'MovieBase'
    year: int | None

class MovieNotRequiredYear(MovieBase):  # Not OK. 'int | None' is not consistent with 'int'
    year: NotRequired[int]

class MovieWithYear(MovieBase):  # OK
    year: NotRequired[int | None]

class BookBase(TypedDict, extra_items=ReadOnly[int | str]):
    title: str

class Book(BookBase, extra_items=str):  # OK
    year: int  # OK

상속 규칙의 중요한 부수 효과는 추가 항목을 허용하지 않는 TypedDict 타입을 정의할 수 있다는 것입니다:

class MovieClosed(TypedDict, extra_items=Never):
    name: str

여기서 extra_items에 Never 값을 전달하면 MovieFinal에 알려진 키 외에 다른 키가 있을 수 없음을 지정합니다. 흔히 사용될 가능성이 있으므로 권장되는 대안이 있습니다:

class MovieClosed(TypedDict, closed=True):
    name: str

이 경우에는 암묵적으로 extra_items=Never로 가정합니다.

할당 가능성

S를 TypedDict 타입에 명시적으로 정의된 항목들의 키 집합이라고 합시다. TypedDict 타입이 extra_items=T를 지정하면, 해당 타입은 다음 조건을 모두 만족하는 무한한 항목 집합을 가진 것으로 간주됩니다.

  • extra_items가 읽기 전용인 경우:
    • 키의 값 타입은 T에 assignable합니다.
    • 키는 S에 속하지 않습니다.
  • extra_items가 읽기 전용이 아닌 경우:
    • 키는 필수가 아닙니다.
    • 키의 값 타입은 T와 consistent합니다.
    • 키는 S에 속하지 않습니다.

타입 검사 목적상, 읽기 전용 항목 섹션에 정의된 규칙에 따라 할당 가능성을 검사할 때 extra_items를 필수가 아닌 의사 항목(pseudo-item)으로 간주하며, 다음과 같이 굵은 글씨로 표시한 새 규칙을 추가합니다:

B가 A에 구조적으로 할당 가능하면 TypedDict 타입 B는 TypedDict 타입 A에 할당 가능(assignable)합니다. 이는 다음 조건을 모두 만족할 때, 그리고 그때에만 참입니다:
  • [``B``에서 같은 이름의 키를 찾을 수 없으면 ‘extra_items’ 인자를 해당 키의 값 타입으로 간주합니다.]
  • A의 각 항목에 대해, A의 항목이 읽기 전용이고 필수가 아니며 최상위 값 타입(ReadOnly[NotRequired[object]])인 경우가 아니라면 B에 해당 키가 있습니다.
  • A의 각 항목에 대해, B에 해당 키가 있으면 B의 해당 값 타입은 A의 값 타입에 할당 가능합니다.
  • A의 읽기 전용이 아닌 각 항목에 대해, 그 값 타입은 B의 해당 값 타입에 할당 가능하며 해당 키는 B에서 읽기 전용이 아닙니다.
  • A의 각 필수 키에 대해, 해당 키는 B에서도 필수입니다.
  • A의 필수가 아닌 각 키에 대해, 그 항목이 A에서 읽기 전용이 아니면 해당 키는 B에서 필수가 아닙니다.

다음 예제는 이러한 검사가 실제로 어떻게 적용되는지 보여 줍니다.

extra_items는 할당 가능성 검사에서 추가 항목에 여러 가지 제약을 둡니다:

class Movie(TypedDict, extra_items=int | None):
    name: str

class MovieDetails(TypedDict, extra_items=int | None):
    name: str
    year: NotRequired[int]

details: MovieDetails = {"name": "Kill Bill Vol. 1", "year": 2003}
movie: Movie = details  # Not OK. While 'int' is assignable to 'int | None',
                        # 'int | None' is not assignable to 'int'

class MovieWithYear(TypedDict, extra_items=int | None):
    name: str
    year: int | None

details: MovieWithYear = {"name": "Kill Bill Vol. 1", "year": 2003}
movie: Movie = details  # Not OK. 'year' is not required in 'Movie',
                        # but it is required in 'MovieWithYear'

여기서 MovieWithYear (B)는 다음 규칙에 따라 Movie (A)에 할당 가능하지 않습니다:

  • A의 필수가 아닌 각 키에 대해, 그 항목이 A에서 읽기 전용이 아니면 해당 키는 B에서 필수가 아닙니다.

TypedDict 타입에서 extra_items를 읽기 전용으로 지정하면 항목이 extra_items 인자보다 더 좁은 타입을 가질 수 있습니다:

class Movie(TypedDict, extra_items=ReadOnly[str | int]):
    name: str

class MovieDetails(TypedDict, extra_items=int):
    name: str
    year: NotRequired[int]

details: MovieDetails = {"name": "Kill Bill Vol. 2", "year": 2004}
movie: Movie = details  # OK. 'int' is assignable to 'str | int'.

이는 year: ReadOnly[str | int]가 Movie에 명시적으로 정의된 항목인 것과 같은 방식으로 동작합니다.

유사 항목으로서의 extra_items는 다른 항목과 같은 규칙을 따르므로, 두 TypedDict 타입이 모두 extra_items를 지정하면 이 검사가 자연스럽게 적용됩니다:

class MovieExtraInt(TypedDict, extra_items=int):
    name: str

class MovieExtraStr(TypedDict, extra_items=str):
    name: str

extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
extra_str: MovieExtraStr = {"name": "No Country for Old Men", "description": ""}
extra_int = extra_str  # Not OK. 'str' is not assignable to extra items type 'int'
extra_str = extra_int  # Not OK. 'int' is not assignable to extra items type 'str'

닫히지 않은 TypedDict 타입은 값 타입이 ReadOnly[object]인 필수가 아닌 추가 키를 암묵적으로 허용합니다. 이 타입과 닫힌 TypedDict 타입 사이에 할당 가능성 규칙을 적용하는 것은 허용됩니다:

class MovieNotClosed(TypedDict):
    name: str

extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
not_closed: MovieNotClosed = {"name": "No Country for Old Men"}
extra_int = not_closed  # Not OK.
                        # 'extra_items=ReadOnly[object]' implicitly on 'MovieNotClosed'
                        # is not assignable to with 'extra_items=int'
not_closed = extra_int  # OK

생성자와의 상호 작용

T 타입의 추가 항목을 허용하는 TypedDict는 클래스 객체를 호출하여 생성할 때 이 타입의 임의 키워드 인자도 허용합니다:

class NonClosedMovie(TypedDict):
    name: str

NonClosedMovie(name="No Country for Old Men")  # OK
NonClosedMovie(name="No Country for Old Men", year=2007)  # Not OK. Unrecognized item

class ExtraMovie(TypedDict, extra_items=int):
    name: str

ExtraMovie(name="No Country for Old Men")  # OK
ExtraMovie(name="No Country for Old Men", year=2007)  # OK
ExtraMovie(
    name="No Country for Old Men",
    language="English",
)  # Not OK. Wrong type for extra item 'language'

# This implies 'extra_items=Never',
# so extra keyword arguments would produce an error
class ClosedMovie(TypedDict, closed=True):
    name: str

ClosedMovie(name="No Country for Old Men")  # OK
ClosedMovie(
    name="No Country for Old Men",
    year=2007,
)  # Not OK. Extra items not allowed

지원되는 연산과 지원되지 않는 연산

타입 명세의 다음 서술은 여전히 유효합니다.

(문자열 리터럴이나 값이 알려진 다른 문자열 표현식이 아닌) 임의의 str 키를 사용하는 연산은 일반적으로 거부해야 합니다.

NotRequired 항목에 이미 적용되는 연산은 타입 명세의 같은 근거에 따라 일반적으로 추가 항목에도 적용되어야 합니다:

정확한 타입 검사 규칙은 각 타입 검사기가 결정합니다. 어떤 경우에는, 그 대안이 관용적인 코드에 대해 거짓 양성 오류를 내는 것이라면 잠재적으로 안전하지 않은 연산을 받아들일 수도 있습니다.

TypedDict가 Mapping[str, VT] 또는 dict[str, VT]에 할당 가능(assignable)하기 때문에, 임의의 str 키를 사용한 인덱스 접근과 할당을 포함한 일부 연산이 허용될 수 있습니다. 이어지는 두 절에서 이를 자세히 설명합니다.

Mapping[str, VT] 매핑과의 상호 작용

TypedDict에 있는 항목의 값 타입이 모두 VT에 할당 가능하면, 그 TypedDict 타입은 Mapping[str, VT] 형태의 타입에 할당 가능(assignable)합니다. 이 규칙을 적용할 때 extra_items=나 closed=가 설정되지 않은 TypedDict는 값 타입이 ReadOnly[object]인 항목을 가진 것으로 간주합니다. 이는 타입 명세에 있는 현재의 할당 가능성 규칙을 확장합니다.

예를 들면 다음과 같습니다:

class MovieExtraStr(TypedDict, extra_items=str):
    name: str

extra_str: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
str_mapping: Mapping[str, str] = extra_str  # OK

class MovieExtraInt(TypedDict, extra_items=int):
    name: str

extra_int: MovieExtraInt = {"name": "Blade Runner", "year": 1982}
int_mapping: Mapping[str, int] = extra_int  # Not OK. 'int | str' is not assignable with 'int'
int_str_mapping: Mapping[str, int | str] = extra_int  # OK

타입 검사기는 이러한 TypedDict 타입에서 values()와 items()의 정확한 시그니처를 추론해야 합니다.:

def foo(movie: MovieExtraInt) -> None:
    reveal_type(movie.items())  # Revealed type is 'dict_items[str, str | int]'
    reveal_type(movie.values())  # Revealed type is 'dict_values[str, str | int]'

이 할당 가능성 규칙을 확장하면, extra_items 또는 closed=True가 지정된 경우 타입 검사기는 임의의 str 키를 사용한 인덱스 접근을 허용할 수 있습니다. 예를 들면 다음과 같습니다:

def bar(movie: MovieExtraInt, key: str) -> None:
    reveal_type(movie[key])  # Revealed type is 'str | int'

TypedDict의 타입 좁히기 동작을 정의하는 것은 이 PEP의 범위를 벗어납니다. 따라서 타입 검사기는 임의의 str 키를 사용한 인덱스 접근을 더 엄격하게 또는 덜 엄격하게 다룰 수 있는 유연성을 가집니다. 예를 들어, 타입 검사기는 명시적인 'x' in d 검사를 요구하여 더 엄격하게 제한하는 쪽을 선택할 수 있습니다.

dict[str, VT]와의 상호 작용

닫힌 TypedDict 타입에 extra_items가 있으면 그 구조적(structural) 서브타입(subtypes)에 추가 필수 키를 둘 수 없으므로, 정적 분석 중에 해당 TypedDict 타입과 그 구조적 서브타입이 필수 키를 가질 가능성이 있는지 판단할 수 있습니다.

TypedDict 타입의 모든 항목이 다음 조건을 만족하면, 그 TypedDict 타입은 dict[str, VT]에 할당 가능(assignable)합니다:

  • 항목의 값 타입이 VT와 일관(consistent)됩니다.
  • 항목이 읽기 전용이 아닙니다.
  • 항목이 필수가 아닙니다.

예를 들면 다음과 같습니다:

class IntDict(TypedDict, extra_items=int):
    pass

class IntDictWithNum(IntDict):
    num: NotRequired[int]

def f(x: IntDict) -> None:
    v: dict[str, int] = x  # OK
    v.clear()  # OK

not_required_num_dict: IntDictWithNum = {"num": 1, "bar": 2}
regular_dict: dict[str, int] = not_required_num_dict  # OK
f(not_required_num_dict)  # OK

이 경우, 이전에는 TypedDict에서 사용할 수 없었던 메서드가 dict[str, VT]와 일치하는 시그니처로 허용됩니다(예: __setitem__(self, key: str, value: VT) -> None):

not_required_num_dict.clear()  # OK

reveal_type(not_required_num_dict.popitem())  # OK. Revealed type is 'tuple[str, int]'

def f(not_required_num_dict: IntDictWithNum, key: str):
    not_required_num_dict[key] = 42  # OK
    del not_required_num_dict[key]  # OK

이전 절의 인덱스 접근에 관한 참고 사항은 여전히 적용됩니다.

dict[str, VT]는 TypedDict 타입에 할당할 수 없습니다. 그러한 dict는 dict의 서브타입일 수 있기 때문입니다:

class CustomDict(dict[str, int]):
    pass

def f(might_not_be_a_builtin_dict: dict[str, int]):
    int_dict: IntDict = might_not_be_a_builtin_dict # Not OK

not_a_builtin_dict = CustomDict({"num": 1})
f(not_a_builtin_dict)

런타임 동작

런타임에는 클래스 문법을 사용하든 함수형 문법을 사용하든, 같은 TypedDict 정의에 closed와 extra_items 인자를 모두 전달하면 오류가 발생합니다. 단순화를 위해, 런타임은 상속과 관련된 다른 잘못된 조합은 검사하지 않습니다.

인트로스펙션을 위해 closed와 extra_items 인자는 결과로 생성되는 TypedDict 객체의 두 가지 새 속성인 __closed__와 __extra_items__에 매핑됩니다. 이 속성들은 슈퍼클래스를 고려하지 않고 TypedDict 생성자에 전달된 값을 그대로 반영합니다.

closed가 전달되지 않으면 __closed__의 값은 None입니다. extra_items가 전달되지 않으면 __extra_items__의 값은 새로운 센티널 객체인 typing.NoExtraItems입니다. (이 값은 None일 수 없습니다. extra_items=None은 모든 추가 항목이 None이어야 함을 나타내는 유효한 정의이기 때문입니다.)

교육 방법

이 PEP에서 도입된 새로운 기능은 TypedDict에 적용되는 상속 개념과 함께 가르칠 수 있습니다. 가능한 개요는 다음과 같습니다:

  • TypedDict의 기초: 고정된 키 집합과 값 타입을 가진 dict입니다.
  • NotRequired, Required, total=False: 누락될 수 있는 키입니다.
  • ReadOnly: 수정할 수 없는 키입니다.
  • 상속: 서브클래스는 새로운 키를 추가할 수 있습니다. 그 결과로, TypedDict 타입의 값은 런타임에 해당 타입에 명시되지 않은 추가 키를 포함할 수 있습니다.
  • closed=True: 추가 키를 허용하지 않고 상속을 제한합니다.
  • extra_items=VT: 지정한 값 타입을 가진 추가 키를 허용합니다.

닫힌 TypedDict의 개념은 관련 개념을 다루는 문서에서도 상호 참조되어야 합니다. 예를 들어, in 연산자를 사용한 타입 좁히기는 닫힌 TypedDict 타입에서 다르게, 어쩌면 더 직관적으로 동작합니다. 또한 키워드 인자에 Unpack을 사용할 때, 닫힌 TypedDict는 허용되는 키워드 인자를 제한하는 데 유용할 수 있습니다.

하위 호환성

extra_items는 명시적으로 선택해야 사용되는 기능이므로, 이 변경으로 인해 기존 코드베이스가 깨지는 일은 없습니다.

TD = TypedDict("TD", foo=str, bar=int)와 같은 형태를 사용할 때 키워드 인자로 쓰인 closed와 extra_items가 다른 키와 충돌하지 않는다는 점에 유의하십시오. 이 문법은 Python 3.13에서 이미 제거되었기 때문입니다.

이는 타입 검사 기능이므로, 타입 검사기가 지원하기만 하면 이전 버전에서도 사용할 수 있습니다.

기각된 아이디어

closed 클래스 매개변수 대신 @final 사용하기

이 내용은 여기에서 논의되었습니다.

Eric Traut의 관련 댓글을 인용합니다:

@final 클래스 데코레이터는 클래스를 서브클래싱할 수 없음을 나타냅니다. 이는 명목적 타입을 정의하는 클래스에는 타당합니다. 그러나 TypedDict는 Protocol과 비슷한 구조적 타입인 프로토콜의 일종이 아니라, 프로토콜과 유사한 구조적 타입입니다. 즉, 이름은 다르지만 필드 정의가 같은 두 TypedDict 클래스는 동등한 타입입니다. 타입 일관성을 판단할 때 이들의 이름과 계층 구조는 중요하지 않습니다. 그런 이유로 @final은 TypedDict의 타입 일관성 규칙에 아무런 영향을 주지 않으며, 항목이나 값의 동작을 바꾸어서도 안 됩니다.

closed 클래스 매개변수와 함께 특수한 __extra_items__ 키 사용하기

이 제안의 이전 개정판에서는 다음과 같이 __extra_items__의 값 타입을 활용하여 허용되는 추가 항목의 타입을 지정하는 방식을 논의했습니다:

class IntDict(TypedDict, closed=True):
    __extra_items__: int

여기서 키 충돌을 피하기 위해, __extra_items__가 특별하게 취급되려면 closed=True가 필요합니다.

커뮤니티의 일부 구성원은 이 문법의 우아함에 대해 우려를 표했습니다. 실제로 일반 키와의 충돌은 우회 방법으로 완화할 수 있지만, 예약된 키를 사용하는 것이 이 제안의 핵심이므로 이러한 우려를 해소할 수 있는 방법은 제한적입니다.

키를 지정하는 새로운 문법 지원하기

문자열 키를 지정할 수 있는 새로운 문법을 도입하면, TypedDict 타입을 정의하는 함수형 문법을 폐기할 수 있고, 추가 항목의 타입을 지정하기 위해 특수 키를 예약하기로 결정할 경우의 키 충돌 문제도 해결할 수 있습니다.

예를 들면 다음과 같습니다:

class Foo(TypedDict):
    name: str  # Regular item
    _: bool    # Type of extra items
    __items__ = {
        "_": int,   # Literal "_" as a key
        "class": str,  # Keyword as a key
        "tricky.name?": float,  # Arbitrary str key
    }

이는 Jukka가 여기에서 제안했습니다. '_' 키는 새로운 이름을 만들어 낼 필요가 없고 match 문과 유사하다는 점 때문에 선택되었습니다.

이렇게 하면 TypedDict 타입을 정의하는 함수형 문법을 완전히 폐기할 수 있지만, 몇 가지 단점이 있습니다. 예를 들어:

  • extra_items=bool과 같은 클래스 인자를 추가하는 것에 비해, _: bool이 TypedDict를 특별하게 만든다는 점이 독자에게 덜 분명합니다.
  • _: bool 키를 사용하는 기존 TypedDict와 하위 호환되지 않습니다. 그러한 사용자에게도 이 문제를 피할 방법이 있기는 하지만, Python(또는 typing-extensions)을 업그레이드하면 여전히 문제가 됩니다.
  • 이 타입들은 어노테이션 컨텍스트에 나타나지 않으므로 평가가 지연되지 않습니다.

타입을 지정하지 않고 추가 항목 허용하기

extra=True는 원래 total=True가 동작하는 방식처럼 타입과 관계없이 추가 항목을 받아들이는 TypedDict를 정의하기 위해 제안되었습니다:

class ExtraDict(TypedDict, extra=True):
    pass

이 방식은 추가 항목의 타입을 지정할 방법을 제공하지 않았기 때문에 타입 검사기는 추가 항목의 타입을 Any로 가정해야 하며, 이는 타입 안전성을 해칩니다. 게다가 TypedDict의 현재 동작은 structural 할당 가능성 때문에 타입이 지정되지 않은 추가 항목이 런타임에 존재하는 것을 이미 허용합니다. 현재 제안에서는 closed=True가 이와 비슷한 역할을 합니다.

교차 타입으로 추가 항목 지원하기

Python 타입 시스템에서 교차 타입을 지원하려면 많은 신중한 검토가 필요하며, 커뮤니티가 합리적인 설계에 대해 합의에 이르기까지 오랜 시간이 걸릴 수 있습니다.

이상적으로는 TypedDict의 추가 항목이 교차 타입에 관한 작업 때문에 막혀서는 안 되며, 반드시 교차 타입을 통해 지원되어야 하는 것도 아닙니다.

또한 Mapping[...]과 TypedDict의 교집합은 제안된 extra_items 특수 항목을 가진 TypedDict 타입과 동등하지 않습니다. TypedDict에 있는 모든 알려진 항목의 값 타입이 Mapping[...]의 값 타입에 대해 서브타입 관계(is-subtype-of)를 만족해야 하기 때문입니다.

알려진 항목과 extra_items 사이의 타입 호환성 요구하기

extra_items는 TypedDict 타입에 알려지지 않은 키의 값 타입을 제한합니다. 따라서 알려진 항목의 값 타입이 반드시 extra_items에 할당 가능한 것은 아니며, extra_items 역시 반드시 모든 알려진 항목의 값 타입에 할당 가능한 것은 아닙니다.

이는 TypeScript의 Index Signatures 문법과 다릅니다. TypeScript의 이 문법에서는 모든 프로퍼티의 타입이 문자열 인덱스의 타입과 일치해야 합니다. 예를 들면 다음과 같습니다.

interface MovieWithExtraNumber {
    name: string // Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
    [index: string]: number
}

interface MovieWithExtraNumberOrString {
    name: string // OK
    [index: string]: number | string
}

이 제약은 임의의 키로 건전한 인덱스 접근을 할 수 있게 해 주지만, TypeScript의 이슈 트래커에서 논의된 사용성 제한이 따릅니다. MovieWithExtraNumber와 같은 타입을 정의할 수 있도록, 정의된 키를 인덱스 시그니처에서 제외할 수 있게 하자는 제안이 있었습니다. 이는 아마도 뺄셈 타입을 수반할 것이며, 이는 이 PEP의 범위를 벗어납니다.

참조 구현

이 기능은 pyright 1.1.386에서 지원되며, 이전 개정판은 pyanalyze 0.12.0에서 지원됩니다.

이 기능은 typing-extensions 4.13.0에서도 지원됩니다.

감사의 글

이 PEP를 후원하고 리뷰 피드백을 제공해 준 Jelle Zijlstra, 이 PEP가 발전시킨 원래 설계를 제안한 Eric Traut, 그리고 PEP 705의 저자로서 자신의 관점을 제시해 준 Alice Purcell에게 감사드립니다.