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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 closed 및 extra_items라는 두 클래스 매개변수를 추가하여 TypedDict의 추가 항목에 타입을 지정합니다. 이를 통해 닫힌 TypedDict 타입을 정의하거나, 지정된 타입의 추가 항목을 허용하면서 dict에 나타날 수 있는 키의 일부에 타입을 지정할 수 있습니다.
동기
A typing.TypedDict 타입은 딕셔너리의 각 알려진 항목의 값 타입에 타입 어노테이션을 지정할 수 있습니다. 그러나 structuralassignability로 인해 TypedDict에는 해당 타입을 통해 표시되지 않는 추가 항목이 있을 수 있습니다. 현재는 TypedDict 타입의 consistent subtypes에 존재할 수 있는 항목의 타입을 제한할 방법이 없습니다.
추가 항목의 명시적 금지
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를 생성할 때는 이 제한이 적용되지만, structuralassignability로 인해 TypedDict에는 해당 타입을 통해 표시되지 않는 추가 항목이 있을 수 있습니다. 예를 들면 다음과 같습니다.:
class Movie(MovieBase):
year: int
movie: Movie = {"name": "Blade Runner", "year": 1982}
foo(movie) # OK
in 검사를 통해 추가 항목의 존재를 확인하고 타입 안전성을 해치지 않으면서 해당 항목에 액세스하는 것은 불가능합니다. 이는 MovieBase의 일부 consistent subtypes에서 해당 항목이 존재할 수 있는 경우에도 마찬가지입니다.:
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는 Unpack을 사용하는 TypedDict를 통해 **kwargs로 표현되는 개별 키워드 인자의 타입에 정확하게 어노테이션을 지정하는 방법을 추가합니다. 그러나 TypedDict는 임의의 추가 항목을 받아들이도록 정의할 수 없으므로, TypedDict를 정의할 당시에는 알려지지 않은 추가 키워드 인자를 허용할 수 없습니다.
기존 코드베이스에서 **kwargs에 사전 PEP 692타입 어노테이션을 사용하고 있다는 점을 고려하면, 이전의 타입 지정 동작을 Unpack과 함께 지원할 수 있도록 TypedDict에서 추가 항목을 받아들이고 타입을 지정하는 것이 유용합니다.
이전 논의
이 PEP에서 도입되는 새로운 기능은 타입 시스템에서 오랫동안 제기되어 온 여러 기능 요청을 해결합니다. 이전 논의에는 다음이 포함됩니다.
- 최종 TypedDict를 요청한 Mypy 이슈(2019년)가 있습니다. 논의는
@final데코레이터에 초점을 맞추지만, 이 PEP를 통해 근본적인 기능 요청이 해결됩니다. - Mailing list thread asking for a way to say that a
TypedDict가 임의의 추가 키를 포함할 수 있음을 표현하는 방법을 요청한 메일링 리스트 스레드(2020)입니다. - Discussion은 PEP 692 (2023)에서 도입된
Unpack메커니즘의 확장에 관한 논의입니다. - PEP 705의 이전 초안에서는 유사한 기능을 제안했지만(2023), 해당 PEP를 더 단순하게 유지하기 위해 제거되었습니다.
- “정확한”
TypedDict에 관한 Discussion(2024)입니다.
근거
TypedDict에서 str형식의 추가 항목을 허용하는 타입이 필요하다고 가정하십시오.
TypeScript의 Index Signatures는 이를 허용합니다.
type Foo = {
a: string
[key: string]: string
}
이 제안은 구문을 변경하지 않고 유사한 기능을 지원하며, 기존 할당 가능성 규칙을 자연스럽게 확장하는 것을 목표로 합니다.
TypedDict에 extra_items 클래스 매개변수를 추가할 것을 제안합니다. 이 매개변수는 type expression을 인자로 받습니다. 이 매개변수가 있으면 추가 항목이 허용되며, 해당 항목의 값 타입은 타입 표현식의 값에 할당 가능해야 합니다.
이를 적용하면 추가 항목을 허용하지 않을 수 있습니다. 인자로 리터럴 True 또는 False만 허용하는 closed 클래스 매개변수를 추가할 것을 제안합니다. closed와 extra_items를 동시에 사용하면 런타임 오류가 발생해야 합니다.
인덱스 시그니처와 달리 알려진 항목의 타입은 extra_items인자에 할당 가능할 필요가 없습니다.
이 접근 방식에는 몇 가지 장점이 있습니다.
extra_items를 의사 항목으로 취급할 수 있는 typing 사양에 정의된 할당 가능성 규칙을 기반으로 구축할 수 있습니다.- 추가 항목의 타입을 지정하기 위해 문법을 변경할 필요가 없습니다.
- 알려진 항목의 값 타입이 assignable
extra_items에 할당 가능해야 한다고 요구하지 않고도 추가 항목의 타입을 정확하게 지정할 수 있습니다. 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의 typing spec에 언급된 대로 계속 포착되어야 합니다.
closed=True를 설정하면 추가 항목이 허용되지 않습니다. 이는 extra_items=Never와 동등합니다. 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
이에 대해서는 a later section에서 추가로 논의합니다.
함수형 구문에서도 closed를 지원합니다.:
Movie = TypedDict("Movie", {"name": str}, closed=True)
전체성(총량)과의 상호 작용
extra_items와 함께 Required[] 또는 NotRequired[]를 사용하는 것은 오류입니다. total=False와 total=True는 extra_items 자체에 영향을 주지 않습니다.
TypedDict의 totality와 관계없이 추가 항목은 비필수입니다. NotRequired 항목에 사용할 수 있는 Operations은 추가 항목에도 사용할 수 있어야 합니다.:
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의 추가 항목은 읽기 전용 항목의 속성을 갖습니다. 이는 Read-only Items에 지정된 상속 규칙과 상호 작용합니다.
특히 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이 읽기 전용인 경우- 해당 항목은 필수 항목이거나 비필수 항목일 수 있습니다.
- 해당 항목의 값 타입은 assignable하여
T에 할당할 수 있어야 합니다.
extra_items이 읽기 전용이 아닌 경우- 해당 항목은 비필수 항목입니다.
- 해당 항목의 값 타입은 consistent하여
T와 일관되어야 합니다.
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 타입에 명시적으로 정의된 항목의 키 집합이라고 하겠습니다. extra_items=T를 지정하면 TypedDict 타입에는 다음 조건을 모두 만족하는 항목이 무한히 많이 있는 것으로 간주합니다.
extra_items이 읽기 전용인 경우:- 키의 값 타입은 assignable하여
T에 할당할 수 있어야 합니다. - 키는
S에 포함되지 않습니다.
- 키의 값 타입은 assignable하여
extra_items이 읽기 전용이 아닌 경우:- 키는 비필수 항목입니다.
- 키의 값 타입은 consistent하여
T와 일관되어야 합니다. - 키는
S에 포함되지 않습니다.
타입 검사 목적상 읽기 전용 항목 섹션에 정의된 규칙에 따라 할당 가능성을 검사할 때 extra_items을 비필수 의사 항목으로 취급하며, 다음과 같이 굵은 글씨로 추가된 새 규칙을 적용합니다:
TypedDict 형식B는A에 할당 가능하며,B가A에 구조적으로 할당 가능할 경우입니다. 다음 조건을 모두 충족하는 경우에만 참입니다.
- [``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
지원되는 연산과 지원되지 않는 연산
typing spec의 다음 명세는 여전히 유효합니다.
임의의 str 키(문자열 리터럴이나 문자열 값을 알고 있는 다른 식 대신)를 사용하는 연산은 일반적으로 거부해야 합니다.
이미 NotRequired항목에 적용되는 연산은 typing spec의 동일한 근거에 따라 일반적으로 추가 항목에도 적용해야 합니다.
정확한 타입 검사 규칙은 각 타입 검사기가 결정할 사항입니다. 경우에 따라 관용적인 코드에 오탐 오류를 생성하는 것보다 잠재적으로 안전하지 않은 연산을 허용하는 편이 나으므로 이를 허용할 수 있습니다.
임의의 str 키를 사용하는 인덱싱 및 할당을 비롯한 일부 연산은 TypedDict가 Mapping[str, VT]또는 dict[str, VT]에 할당 가능하기 때문에 허용될 수 있습니다. 다음 두 절에서 이를 자세히 설명합니다.
매핑[str, VT]과의 상호 작용
TypedDict 형식의 항목에 있는 모든 값 형식이 VT에 할당 가능할 때, TypedDict 형식은 Mapping[str, VT]형식에 할당 가능합니다. 이 규칙의 목적상 extra_items=또는 closed=가 설정되지 않은 TypedDict는 값 형식이 ReadOnly[object]인 항목을 가진 것으로 간주합니다. 이는 typing spec의 기존 할당 가능성 규칙을 확장합니다.
예를 들어::
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의 서브타입일 수 있기 때문입니다.:
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의 개념은 관련 개념에 대한 문서에서도 상호 참조해야 합니다. 예를 들어, 닫힌 TypedDict 유형에서는 in 연산자를 사용한 타입 좁히기가 다르게, 어쩌면 더 직관적으로 작동합니다. 또한 키워드 인자에 Unpack을 사용할 때 닫힌 TypedDict는 허용되는 키워드 인자를 제한하는 데 유용할 수 있습니다.
하위 호환성
extra_items는 선택적으로 활성화하는 기능이므로, 이 변경으로 인해 기존 코드베이스가 중단되지 않습니다.
closed와 extra_items를 키워드 인자로 사용할 때 TD = TypedDict("TD", foo=str, bar=int)와 같은 구문에서는 다른 키와 충돌하지 않는다는 점에 유의하십시오. 이 구문은 Python 3.13에서 이미 제거되었기 때문입니다.
이는 타입 검사 기능이므로, 타입 검사기가 지원하는 한 이전 버전에서도 사용할 수 있습니다.
거부된 아이디어
closed 클래스 매개변수 대신 @final사용
이는 here에서 논의되었습니다.
Eric Traut의 관련 comment을 인용하면 다음과 같습니다.
@final 클래스 데코레이터는 클래스를 서브클래싱할 수 없음을 나타냅니다. 이는 명목 타입을 정의하는 클래스에 적합합니다. 그러나 TypedDict는 프로토콜과 유사한 구조적 타입입니다. 즉, 이름은 다르지만 필드 정의가 동일한 두 TypedDict 클래스는 동등한 타입입니다. 타입 일관성을 판단할 때 해당 클래스의 이름과 계층 구조는 중요하지 않습니다. 따라서 @final은 TypedDict 타입 일관성 규칙에 아무런 영향을 주지 않으며, 항목이나 값의 동작을 변경해서도 안 됩니다.
closed 클래스 매개변수와 함께 특수 __extra_items__ 키 사용
이 제안의 초기 개정판에서는 허용되는 추가 항목의 유형을 지정하기 위해 __extra_items__의 값 유형을 활용하는 접근 방식을 논의했습니다. 예를 들면 다음과 같습니다.:
class IntDict(TypedDict, closed=True):
__extra_items__: int
여기서 키 충돌을 피하려면 closed=True가 필요하며, 그래야 __extra_items__를 특별히 처리할 수 있습니다.
커뮤니티의 일부 구성원은 이 구문의 우아함에 우려를 표합니다. 실질적으로는 우회 방법을 통해 일반 키와의 키 충돌을 완화할 수 있지만, 예약 키의 사용이 이 제안의 핵심이므로 이러한 우려를 해결할 방법은 제한적입니다.
키를 지정하는 새로운 구문 지원
문자열 키를 지정할 수 있는 새로운 구문을 도입하면, 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
}
이는 here by Jukka에서 Jukka가 제안했습니다. '_' 키는 새 이름을 만들어낼 필요가 없고 match 문과 유사하기 때문에 선택되었습니다.
이를 통해 TypedDict 유형을 정의하는 함수형 구문을 완전히 더 이상 사용하지 않도록 할 수 있지만, 몇 가지 단점이 있습니다. For example:
- 독자에게
_: bool가extra_items=bool과 같은 클래스 인자를 추가하는 것에 비해 TypedDict를 특수하게 만든다는 점이 덜 분명합니다. - 이는
_: bool키를 사용하는 기존 TypedDict와 하위 호환되지 않습니다. 이러한 사용자가 문제를 피할 방법은 있지만, Python(또는 typing-extensions)을 업그레이드하면 여전히 문제가 됩니다. - 해당 타입들은 어노테이션 컨텍스트에 나타나지 않으므로 평가가 지연되지 않습니다.
타입을 지정하지 않고 추가 항목 허용하기
extra=True는 total=True가 작동하는 방식과 같이 타입에 관계없이 추가 항목을 허용하는 TypedDict를 정의하기 위한 것으로 처음 제안되었습니다.:
class ExtraDict(TypedDict, extra=True):
pass
추가 항목의 타입을 지정할 방법을 제공하지 않았으므로, 타입 검사기는 추가 항목의 타입이 Any라고 가정해야 하며, 이는 타입 안전성을 저해합니다. 또한 현재 TypedDict의 동작은 structural로 인해 타입이 지정되지 않은 추가 항목이 런타임에 존재하는 것을 이미 허용합니다. assignability. 현재 제안에서 closed=True는 이와 유사한 역할을 합니다.
교집합을 사용한 추가 항목 지원
Python의 타입 시스템에서 교집합을 지원하려면 신중한 검토가 많이 필요하며, 커뮤니티가 합리적인 설계에 대한 합의에 도달하는 데 오랜 시간이 걸릴 수 있습니다.
이상적으로는 TypedDict의 추가 항목이 교집합에 관한 작업으로 차단되어서는 안 되며, 반드시 교집합을 통해 지원해야 하는 것도 아닙니다.
더욱이 Mapping[...]과 TypedDict의 교집합은 제안된 extra_items 특수 항목을 사용하는 TypedDict 타입과 동등하지 않습니다. TypedDict의 모든 알려진 항목의 값 타입이 Mapping[...]의 값 타입과 is-subtype-of 관계를 만족해야 하기 때문입니다.
알려진 항목과 extra_items간의 타입 호환성 요구
extra_items는 TypedDict 타입에 unknown인 키의 값 타입을 제한합니다. 따라서 모든 known항목의 값 타입이 extra_items에 할당 가능한 것은 아니며, extra_items가 알려진 모든 항목의 값 타입에 할당 가능한 것도 아닙니다.
이는 모든 속성의 타입이 문자열 인덱스의 타입과 일치해야 하는 TypeScript의 Index Signatures 구문과 다릅니다. 예를 들면 다음과 같습니다.
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’s issue tracker 에서 논의된 사용성 제한이 따릅니다. 정의된 키를 인덱스 시그니처에서 제외할 수 있도록 하여 MovieWithExtraNumber와 같은 타입을 정의하자는 제안이 있었습니다. 여기에는 뺄셈 타입이 필요할 가능성이 있으며, 이는 이 PEP의 범위를 벗어납니다.
참조 구현
이는 pyright 1.1.386에서 지원되며, 이전 버전은 pyanalyze 0.12.0에서 지원됩니다.
이는 typing-extensions 4.13.0에서도 지원됩니다.
감사의 말
이 PEP를 후원하고 검토 의견을 제공한 Jelle Zijlstra, 이 PEP가 발전시키고 있는 원래 설계를 proposed the original design한 Eric Traut, 그리고 PEP 705의 저자로서 자신의 관점을 제시한 Alice Purcell에게 감사드립니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.