PEP 728 – 带有类型额外项的 TypedDict
- 作者:
- 李子轩 <p359101898 at gmail.com>
- 发起人:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- 讨论至:
- Discourse 帖子
- 状态:
- 已接受
- 类型:
- 标准跟踪
- 主题:
- 类型标注
- 创建日期:
- 2023年9月12日
- Python 版本:
- 3.15
- 发布历史:
- 2024年2月9日
- 决议:
- 2025年8月15日
摘要
本 PEP 为 TypedDict 添加了两个类参数,closed 和 extra_items,用于对 TypedDict 上的额外项进行类型标注。这解决了定义封闭 TypedDict 类型或对 dict 中可能出现的部分键进行类型标注,同时允许指定类型的额外项的需求。
动机
typing.TypedDict 类型可以标注字典中每个已知项的值类型。然而,由于 结构性 可赋值性,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 键,并且在当前规范下,类型检查器收窄其类型将是不正确的。
允许特定类型的额外项
为了支持 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 时强制执行限制,但由于结构性可赋值性,TypedDict 可能包含其类型不可见的额外项。例如:
class Movie(MovieBase):
year: int
movie: Movie = {"name": "Blade Runner", "year": 1982}
foo(movie) # OK
不可能通过 in 检查来确认额外项的存在,并在不破坏类型安全的情况下访问它们,即使它们可能存在于 MovieBase 的某些一致子类型中。
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 时未知的额外关键字参数。
鉴于现有代码库中 PEP 692 之前的 **kwargs 类型标注的使用,接受并对 TypedDict 上的额外项进行类型标注将很有价值,以便旧的类型标注行为可以与 Unpack 结合使用。
以前的讨论
本 PEP 引入的新功能将解决类型系统中一些长期存在的功能请求。以前的讨论包括:
基本原理
假设我们想要一个允许 TypedDict 上额外项类型为 str 的类型。
TypeScript 中的索引签名允许这样做。
type Foo = {
a: string
[key: string]: string
}
本提案旨在在不改变语法的情况下支持类似功能,为现有可赋值性规则提供自然扩展。
我们建议为 TypedDict 添加一个类参数 extra_items。它接受一个类型表达式作为参数;当它存在时,允许额外项,并且它们的值类型必须可赋值给类型表达式值。
其中一个应用是禁止额外项。我们建议添加一个 closed 类参数,它只接受字面量 True 或 False 作为参数。当 closed 和 extra_items 同时使用时,应引发运行时错误。
与索引签名不同,已知项的类型不需要可赋值给 extra_items 参数。
这种方法有一些优点:
- 我们可以在类型规范中定义的可赋值性规则的基础上构建,其中
extra_items可以被视为一个伪项。 - 不需要引入语法更改来指定额外项的类型。
- 我们可以精确地对额外项进行类型标注,而不需要已知项的值类型可赋值给
extra_items。 - 我们不会失去向后兼容性,因为
extra_items和closed都是可选功能。
规范
本规范的结构旨在与 PEP 589 并行,以突出原始 TypedDict 规范的更改。
如果指定了 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 时,不允许额外项。这等同于 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。虽然它们都具有相同的效果,但 closed=True 优于 extra_items=Never。
当 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)
与总体性(Totality)的交互
将 Required[] 或 NotRequired[] 与 extra_items 一起使用是错误的。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[] 类型限定符进行标注时,TypedDict 上的额外项具有只读项的属性。这与 只读项 中指定的继承规则相互作用。
值得注意的是,如果 TypedDict 类型将 extra_items 指定为只读,则 TypedDict 类型的子类可以重新声明 extra_items。
因为非封闭 TypedDict 类型隐式允许值类型为 ReadOnly[object] 的非必需额外项,所以其子类可以用更具体的类型覆盖 extra_items 参数。
更多细节将在后面的部分讨论。
继承
extra_items 的继承方式类似于常规的 key: value_type 项。与其他键一样,继承规则和只读项继承规则适用。
我们需要重新解释这些规则,以定义 extra_items 如何与它们交互。
- 不允许在子类中更改父 TypedDict 类字段的类型。
首先,不允许在子类中更改 extra_items 的值,除非它在超类中声明为 ReadOnly。
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。
- 如果
extra_items不是只读的:- 该项是非必需的。
- 该项的值类型与
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
这里,将值 Never 传递给 extra_items 指定 MovieFinal 中除了已知键之外不能有其他键。由于其潜在的普遍用途,有一个首选的替代方案:
class MovieClosed(TypedDict, closed=True):
name: str
我们隐式假定 extra_items=Never。
可赋值性
设 S 是 TypedDict 类型上明确定义的项的键集。如果它指定了 extra_items=T,则该 TypedDict 类型被认为具有无限项集,所有这些项都满足以下条件。
为了类型检查的目的,在根据 只读项 部分定义的规则检查可赋值性时,extra_items 被视为一个非必需的伪项,并添加了一条粗体新规则如下:
当且仅当以下所有条件都满足时,TypedDict 类型B可赋值给 TypedDict 类型A,且B结构上 可赋值给A:
- [如果在 ``B`` 中找不到同名键,则“extra_items”参数被视为相应键的值类型。]
- 对于
A中的每个项,B都有相应的键,除非A中的项是只读的、非必需的且为顶层值类型(ReadOnly[NotRequired[object]])。- 对于
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
支持和不支持的操作
来自类型规范的此声明仍然有效。
通常应拒绝带有任意字符串键(而不是字符串字面量或其他具有已知字符串值的表达式)的操作。
通常,适用于 NotRequired 项的操作也应适用于额外项,遵循类型规范中的相同原理。
确切的类型检查规则由每个类型检查器决定。在某些情况下,如果替代方案是为惯用代码生成假阳性错误,则可能会接受潜在不安全的操作。
某些操作,包括带有任意字符串键的索引访问和赋值,可能由于 TypedDict 可赋值给 Mapping[str, VT] 或 dict[str, VT] 而被允许。以下两节将对此进行扩展。
与 Mapping[str, VT] 的交互
当 TypedDict 中所有项的值类型都可赋值给 VT 时,TypedDict 类型可赋值给 Mapping[str, VT] 形式的类型。为了此规则的目的,未设置 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 时,类型检查器可以允许使用任意字符串键进行索引访问。例如:
def bar(movie: MovieExtraInt, key: str) -> None:
reveal_type(movie[key]) # Revealed type is 'str | int'
定义 TypedDict 的类型收窄行为超出了本 PEP 的范围。这为类型检查器提供了灵活性,使其对带有任意字符串键的索引访问施加更严格或更宽松的限制。例如,类型检查器可以选择更严格的限制,要求显式的 'x' in d 检查。
与 dict[str, VT] 的交互
因为封闭 TypedDict 类型上 extra_items 的存在禁止其结构子类型中的额外必需键,我们可以在静态分析期间确定 TypedDict 类型及其结构子类型是否会存在任何必需键。
如果 TypedDict 类型上的所有项都满足以下条件,则该 TypedDict 类型可赋值给 dict[str, VT]。
- 项的值类型与
VT一致。 - 该项不是只读的。
- 该项不是必需的。
例如
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 中移除。
由于这是一个类型检查功能,只要类型检查器支持,它就可以用于旧版本。
被拒绝的想法
使用 @final 代替 closed 类参数
这已在此处讨论过。
引用 Eric Traut 的一条相关评论:
@final类装饰器表示一个类不能被子类化。这对于定义名义类型的类来说是有意义的。然而,TypedDict 是一种结构类型,类似于 Protocol。这意味着两个具有不同名称但相同字段定义的 TypedDict 类是等效类型。它们的名称和层次结构对于确定类型一致性并不重要。因此,@final对 TypedDict 类型一致性规则没有影响,也不应改变项或值的行为。
将特殊的 __extra_items__ 键与 closed 类参数结合使用
在本提案的早期修订中,我们讨论了一种方法,该方法将利用 __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
}
Jukka 在此处提出了这个建议。'_' 键之所以被选中,是因为它不需要发明新名称,并且与匹配语句相似。
这将允许我们完全弃用定义 TypedDict 类型的函数式语法,但存在一些缺点。例如:
- 相对于添加像
extra_items=bool这样的类参数,_: bool使 TypedDict 特殊这一点对读者来说不那么明显。 - 它与使用
_: bool键的现有 TypedDict 向后不兼容。虽然这些用户有办法解决这个问题,但如果他们升级 Python(或 typing-extensions),这仍然是他们的问题。 - 类型不出现在注释上下文中,因此它们的评估不会被推迟。
允许额外项而不指定类型
extra=True 最初是为了定义一个接受额外项(无论类型如何)的 TypedDict 而提出的,就像 total=True 的工作方式一样。
class ExtraDict(TypedDict, extra=True):
pass
因为它没有提供指定额外项类型的方法,所以类型检查器需要假定额外项的类型是 Any,这会损害类型安全。此外,由于结构性可赋值性,TypedDict 的当前行为已经允许在运行时存在无类型额外项。closed=True 在当前提案中扮演类似的角色。
支持带交集的额外项
在 Python 的类型系统中支持交集需要大量的仔细考虑,并且社区可能需要很长时间才能就合理的设计达成共识。
理想情况下,TypedDict 中的额外项不应被交集工作所阻碍,也不一定需要通过交集来支持。
此外,Mapping[...] 和 TypedDict 之间的交集不等同于具有提议的 extra_items 特殊项的 TypedDict 类型,因为 TypedDict 中所有已知项的值类型都需要满足与 Mapping[...] 值类型的子类型关系。
要求已知项与 extra_items 的类型兼容
extra_items 限制 TypedDict 类型中*未知*键的值类型。因此,任何*已知*项的值类型不一定可赋值给 extra_items,并且 extra_items 不一定可赋值给所有已知项的值类型。
这与 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 也支持此功能。
致谢
感谢 Jelle Zijlstra 赞助本 PEP 并提供审查反馈,Eric Traut 提出了本 PEP 迭代的原始设计,以及 Alice Purcell 作为 PEP 705 的作者提供了他们的观点。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0728.rst