Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 添加了两个类参数,closedextra_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'

没有什么能阻止一个可赋值给 Moviedict 拥有 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 引入的新功能将解决类型系统中一些长期存在的功能请求。以前的讨论包括:

  • Mypy issue 请求“final TypedDict”(2019)。尽管讨论侧重于 @final 装饰器,但本 PEP 将解决其潜在的功能请求。
  • 邮件列表讨论,要求一种方式来表明 TypedDict 可以包含任意额外键(2020)。
  • 讨论PEP 692 引入的 Unpack 机制的扩展(2023)。
  • PEP 705 在早期草案中提出了类似的功能(2023);为了简化该 PEP 而将其删除。
  • 讨论关于“精确”TypedDict(2024)。

基本原理

假设我们想要一个允许 TypedDict 上额外项类型为 str 的类型。

TypeScript 中的索引签名允许这样做。

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

本提案旨在在不改变语法的情况下支持类似功能,为现有可赋值性规则提供自然扩展。

我们建议为 TypedDict 添加一个类参数 extra_items。它接受一个类型表达式作为参数;当它存在时,允许额外项,并且它们的值类型必须可赋值给类型表达式值。

其中一个应用是禁止额外项。我们建议添加一个 closed 类参数,它只接受字面量 TrueFalse 作为参数。当 closedextra_items 同时使用时,应引发运行时错误。

与索引签名不同,已知项的类型不需要可赋值给 extra_items 参数。

这种方法有一些优点:

  • 我们可以在类型规范中定义的可赋值性规则的基础上构建,其中 extra_items 可以被视为一个伪项。
  • 不需要引入语法更改来指定额外项的类型。
  • 我们可以精确地对额外项进行类型标注,而不需要已知项的值类型可赋值extra_items
  • 我们不会失去向后兼容性,因为 extra_itemsclosed 都是可选功能。

规范

本规范的结构旨在与 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 上定义的额外键,其值类型为 intb 中的 '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 定义中同时使用 closedextra_items 参数是运行时错误。

total 类似,closed 参数的值只支持字面量 TrueFalse。类型检查器应拒绝任何非字面量值。

显式传递 closed=False 请求默认的 TypedDict 行为,其中可能存在任意其他键,并且子类可以添加任意项。如果超类具有 closed=True 或设置了 extra_items,则传递 closed=False 是类型检查器错误。

如果未提供 closed,则行为从超类继承。如果超类是 TypedDict 本身,或者超类没有 closed=Trueextra_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=Falsetotal=Trueextra_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 是只读的:
    • 键的值类型可赋值T
    • 键不在 S 中。
  • 如果 extra_items 不是只读的:
    • 键是非必需的。
    • 键的值类型与 T 一致
    • 键不在 S 中。

为了类型检查的目的,在根据 只读项 部分定义的规则检查可赋值性时,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_itemsclosed=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 定义中传递 closedextra_items 参数是一个错误,无论使用类语法还是函数式语法。为了简单起见,运行时不会检查涉及继承的其他无效组合。

为了自省,closedextra_items 参数映射到结果 TypedDict 对象上的两个新属性:__closed____extra_items__。这些属性准确反映了传递给 TypedDict 构造函数的内容,不考虑超类。

如果未传递 closed,则 __closed__ 的值为 None。如果未传递 extra_items,则 __extra_items__ 的值为新的哨兵对象 typing.NoExtraItems。(它不能是 None,因为 extra_items=None 是一个有效定义,表示所有额外项必须为 None。)

如何教授此内容

本 PEP 中引入的新功能可以与适用于 TypedDict 的继承概念一起教授。可能的提纲如下:

  • TypedDict 的基础知识:具有固定键集和值类型的 dict
  • NotRequiredRequiredtotal=False:可能缺失的键。
  • ReadOnly:不可修改的键。
  • 继承:子类可以添加新键。作为推论,TypedDict 类型的值在运行时可能包含类型中未指定的额外键。
  • closed=True:禁止额外键并限制继承。
  • extra_items=VT:允许带有指定值类型的额外键。

封闭 TypedDict 的概念也应在相关概念的文档中交叉引用。例如,使用 in 运算符进行类型收窄的工作方式与封闭 TypedDict 类型不同,也许更直观。此外,当 Unpack 用于关键字参数时,封闭 TypedDict 可用于限制允许的关键字参数。

向后兼容性

由于 extra_items 是一个可选功能,因此现有代码库不会因这一更改而中断。

请注意,当使用类似 TD = TypedDict("TD", foo=str, bar=int) 时,closedextra_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 的作者提供了他们的观点。


来源:https://github.com/python/peps/blob/main/peps/pep-0728.rst

最后修改:2025-08-18 20:22:33 GMT