Deep Engineering
Продвинутый·Опубликовано·3.12 · 3.13 · 3.14·25 МИН

Типизация декораторов: почему проверяющий замолкает и чем это лечится

Декоратор, объявленный через Callable[..., R], пропускает оба заведомо неверных вызова — и это не поломка проверяющего типов, а ровно то, что многоточие означает. При этом в рантайме сигнатура никуда не девается: inspect показывает исходную. Рантайм честен, а проверяющий слеп — и обычно ждут наоборот.

Полное техническое изложение

TL;DR

  • Декоратор, объявленный через Callable[..., R], пропускает оба заведомо неверных вызова из трёх написанных. Проверяющих типов здесь два — mypy 2.3.1 и pyright 1.1.413, — и молчат оба; это не их поломка: PEP 612 говорит про многоточие прямо — «мы не выполняем никакой проверки аргументов».
  • ParamSpec (PEP 612) на том же коде даёт три ошибки на тех же двух строках: две по типам и одну по числу аргументов. Меняется только объявление декоратора; ни его тело, ни вызовы не трогаются.
  • P.args и P.kwargs существуют только парой, и оба проверяющих отказываются принимать половину. Причина в PEP: два корректных вызова могут разложить один набор параметров по-разному.
  • Рантайм при этом честен, а проверяющий слеп — и обычно ждут ровно наоборот. inspect.signature показывает исходную сигнатуру, потому что functools.wraps оставил __wrapped__, и inspect по нему прыгает.
  • Concatenate умеет добавлять только позиционные параметры. Keyword-only так не добавить, и это ограничение самого PEP, а не проверяющих.
  • Фабрика декораторов — место, где ошибаются почти все: @trace сигнатуру сохраняет, @trace(level=2) теряет её молча. Лечится протоколом с обобщённым __call__.

Ноль ошибок на двух неверных вызовах

Начнём с замера, а не с объяснения. Вот обычный декоратор, написанный так, как его пишут чаще всего:

PYTHON
def add_logging(f: Callable[..., R]) -> Callable[..., R]:
    def inner(*args: object, **kwargs: object) -> R:
        return f(*args, **kwargs)
 
    return inner
 
 
@add_logging
def takes_int_str(x: int, y: str) -> int:
    return x + 7

И три вызова под ним, из которых два заведомо неверны:

PYTHON
takes_int_str(1, "A")  # верный вызов
takes_int_str("B", 2)  # типы переставлены
takes_int_str()  # аргументов нет вовсе

Проверяющий типов — mypy 2.3.1 — не находит ни одного. Файл целиком — bench/typing/01_naive.py, вывод дословно:

$ mypy 01_naive.py
  -> exit=0

Ни ошибки, ни предупреждения, код возврата ноль. Второй проверяющий, pyright 1.1.413, на том же файле тоже молчит.

Что означает многоточие

Обычное объяснение — «... значит любые аргументы» — звучит безобидно и поэтому не настораживает. Документация typing формулирует именно так:

If a literal ellipsis ... is given as the argument list, it indicates that a callable with any arbitrary parameter list would be acceptable.

перевод

Если в качестве списка аргументов указано литеральное многоточие `...`, это означает, что подойдёт вызываемый объект с любым произвольным списком параметров.

typing — Support for type hints

Про то, что происходит с проверкой ВЫЗОВОВ такого объекта, здесь не сказано ничего. А сказано это в PEP 612, и совсем в других словах:

This was not caught by the type checker because the decorated takes_int_str was given the type Callable[..., Awaitable[int]] (an ellipsis in place of parameter types is specified to mean that we do no validation on arguments).

перевод

Это не было отловлено проверяющим типов, потому что декорированной takes_int_str был присвоен тип Callable[..., Awaitable[int]] (многоточие вместо типов параметров, по спецификации, означает, что мы не выполняем никакой проверки аргументов).

PEP 612 — Parameter Specification Variables

Разница между «подойдёт любой» и «не проверяем» — это и есть разница между безобидной формулировкой и тремя пропущенными вызовами. Спецификация системы типов говорит то же самое строже: This is a gradual form indicating that the type is consistent with any input signature (Это градуальная форма, указывающая, что данный тип совместим с любой входной сигнатурой). Градуальная форма — это место, где проверка сознательно отключается.

Заметьте, чего в этом объяснении нет: слова «баг». Проверяющий сделал ровно то, что ему сказали. Сказали неудачно.

ParamSpec: то же самое, три ошибки

PEP 612 существует ровно затем, чтобы это чинить. Постановка задачи в нём — про декораторы прямым текстом:

Neither of these support forwarding the parameter types of one callable over to another callable, making it difficult to annotate function decorators.

перевод

Ни один из них не поддерживает проброс типов параметров одного вызываемого объекта в другой, что затрудняет аннотирование декораторов функций.

PEP 612, Abstract

Тот же декоратор, переписанный через ParamSpec в синтаксисе PEP 695:

PYTHON
def add_logging[**P, R](f: Callable[P, R]) -> Callable[P, R]:
    def inner(*args: P.args, **kwargs: P.kwargs) -> R:
        return f(*args, **kwargs)
 
    return inner

Тело декоратора не изменилось. Вызовы не изменились. Файл вызовов тот же самый. Изменилась одна строка — объявление. Вывод (bench/typing/02_paramspec.py):

02_paramspec.py:22: error: Argument 1 to "takes_int_str" has incompatible type "str"; expected "int"  [arg-type]
02_paramspec.py:22: error: Argument 2 to "takes_int_str" has incompatible type "int"; expected "str"  [arg-type]
02_paramspec.py:23: error: Missing positional arguments "x", "y" in call to "takes_int_str"  [call-arg]

Три ошибки на тех же двух строках — две по типам и одна по числу аргументов. pyright находит те же три, другими словами. Вся статья, в сущности, сводится к этой паре прогонов: ... — не «любые аргументы», а «не проверять».

Почему P.args и P.kwargs только парой

Первое, обо что спотыкаются, — попытка написать половину:

PYTHON
def half[**P, R](f: Callable[P, R]) -> Callable[P, R]:
    def inner(*args: P.args) -> R:  # ошибка: половина пары
        return f(*args)
 
    return inner

Оба проверяющих отказываются, каждый своими словами (bench/typing/07_args_kwargs_pair.py):

07_args_kwargs_pair.py:26: error: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"  [valid-type]

И следом ещё две, на строках 27 и 29: вызов остался без параметров, а результат перестал подходить под объявленный тип. Второй проверяющий отказывается на тех же трёх строках и называет правило прямее:

07_args_kwargs_pair.py:26: "args" and "kwargs" attributes of ParamSpec must both appear within a function signature

Причина названа в PEP, и она не про удобство записи:

A ParamSpec captures both positional and keyword accessible parameters, but there unfortunately is no object in the runtime that captures both of these together.

перевод

ParamSpec захватывает как позиционно, так и по ключу доступные параметры, но, к сожалению, в среде выполнения нет объекта, который захватывал бы и то и другое вместе.

PEP 612, The components of a ParamSpec

Дальше — то, ради чего это ограничение и введено. Один и тот же набор параметров разные вызовы раскладывают по-разному: f(1, y=2) кладёт единицу в args, двойку в kwargs, а f(1, 2) — обе в args.

Therefore, we need to make sure that these special types are only brought into the world together, and are used together, so that our usage is valid for all possible partitions.

перевод

Поэтому мы должны обеспечить, чтобы эти специальные типы появлялись на свет только вместе и использовались вместе — так, чтобы наше использование было корректным для всех возможных разбиений.

Там же

То есть пара — не синтаксическая формальность, а единственный способ описать «те же самые параметры», не зная заранее, как их разложит вызывающий.

Рантайм честен, проверяющий слеп

Здесь начинается то, чего почти нигде не пишут, и оно переворачивает привычную картину.

Пока проверяющий молчал, в рантайме сигнатура никуда не делась. Измерено на 3.12.3, 3.13.7 и 3.14.7 — на всех трёх одинаково:

inspect.signature(takes_int_str)                       -> (x: int, y: str) -> int
inspect.signature(takes_int_str, follow_wrapped=False) -> (*args, **kwargs) -> int
takes_int_str.__code__.co_varnames                     -> ('args', 'kwargs')

Читать это надо снизу вверх. Последняя строка — правда: под декоратором стоит функция, принимающая *args, **kwargs, и вызывать будут именно её. Средняя строка — та же правда, сказанная inspect, когда его просят не прыгать. А верхняя — то, что inspect показывает по умолчанию.

Прыжок возможен из-за functools.wraps — сам механизм разобран в статье про декораторы, здесь он берётся готовым. wraps ставит на обёртку ссылку на обёрнутое (wrapper.__wrapped__ = wrapped, Lib/functools.py:62 на 3.13.7), а inspect.signature объявлен так, что по этой ссылке идёт: def signature(obj, *, follow_wrapped=True, ...) (Lib/inspect.py:3373).

Отсюда и берётся уверенность, что «functools.wraps всё чинит». Он чинит интроспекцию, help() и всё, что читает __wrapped__. Проверяющему он не говорит ничего: wraps — это код, который выполняется при декорировании, а проверяющий смотрит на объявленные типы и до выполнения не доходит.

Получается редкая для Python картина: инструмент времени выполнения знает правду, а инструмент, задача которого — предупредить заранее, не знает. Обычно бывает наоборот, и на этом ожидании ошибка и держится.

Когда декоратор добавляет аргумент

ParamSpec описывает «те же самые параметры». Но декоратор часто меняет сигнатуру: что-то внедряет сам, а наружу это не выставляет. Для таких случаев PEP 612 вводит Concatenate:

The semantics of Concatenate[X, Y, P] are that it represents the parameters represented by P with two positional-only parameters prepended.

перевод

Семантика Concatenate[X, Y, P] такова, что он представляет параметры, представленные P, с двумя добавленными спереди позиционными-только параметрами.

PEP 612, Concatenate

Декоратор, который подставляет соединение с базой, а снаружи его прячет (bench/typing/03_concatenate_and_protocol.py):

PYTHON
def needs_conn[**P, R](f: Callable[Concatenate[str, P], R]) -> Callable[P, R]:
    def inner(*args: P.args, **kwargs: P.kwargs) -> R:
        return f("conn", *args, **kwargs)
 
    return inner
 
 
@needs_conn
def query(conn: str, sql: str, limit: int) -> int:
    return limit

Снаружи у query остаётся два параметра, а не три, и это проверяется:

вызоввердикт
query("select", 10)принят: conn внедрён декоратором
query(10, "select")две ошибки arg-type — порядок типов
query("select", 10, 1)call-arg: Too many arguments for "query"

Тип входа и тип выхода у декоратора теперь разные, и в этом весь смысл: Callable[Concatenate[str, P], R] на входе, Callable[P, R] на выходе.

Чего Concatenate не умеет

Симметричного способа добавить именованный параметр нет. Это не пробел проверяющих и не то, что «пока не сделали в mypy», — ограничение записано в самом PEP отдельным подразделом, вместе с причиной:

However, the key distinction is that while prepending positional-only parameters to a valid callable type always yields another valid callable type, the same cannot be said for adding keyword-only parameters.

перевод

Однако ключевое различие в том, что добавление positional-only параметров в начало корректного вызываемого типа всегда даёт другой корректный вызываемый тип, тогда как про добавление keyword-only параметров того же сказать нельзя.

PEP 612, Concatenating Keyword Parameters

PEP заканчивает подраздел обещанием вернуться к вопросу if there is sufficient demand (если будет достаточный спрос). Пока спроса, видимо, не набралось: в 3.14 ничего не изменилось.

Практический вывод простой. Декоратор, который добавляет именованный параметр, типизируется только протоколом с явно выписанным __call__ — то есть перечнем параметров руками, без ParamSpec.

Декоратор метода: Self переживает

Расхожее «после декоратора цепочки вызовов ломаются» проверяется тем же способом. Concatenate[S, P] сохраняет и получателя, и Self (bench/typing/06_methods.py):

PYTHON
def logged[S, **P, R](m: Callable[Concatenate[S, P], R]) -> Callable[Concatenate[S, P], R]:
    def inner(self: S, *args: P.args, **kwargs: P.kwargs) -> R:
        return m(self, *args, **kwargs)
 
    return inner
 
 
class A:
    @logged
    def m(self, x: int) -> str:
        return str(x)
 
    @logged
    def chain(self) -> Self:
        return self

Вывод:

06_methods.py:24: note: Revealed type is "def (x: int) -> str"
06_methods.py:25: note: Revealed type is "06_methods.A"
06_methods.py:26: error: Argument 1 to "m" of "A" has incompatible type "str"; expected "int"  [arg-type]

Первая строка: у a.m получатель уже связан, снаружи виден один параметр. Вторая: a.chain() даёт A, то есть Self доехал через декоратор. Третья: неверный вызов метода ловится так же, как неверный вызов функции.

Когда декоратор вешает атрибут

Callable[P, R] описывает вызываемое — и только. Если декоратор добавляет объекту атрибут (cache_clear, счётчик, что угодно), в этом типе для атрибута места нет. Решение — протокол, у которого есть и __call__, и всё остальное (вторая половина bench/typing/03_concatenate_and_protocol.py):

PYTHON
class HasCache[**P, R](Protocol):
    __wrapped__: Callable[P, R]
 
    def cache_clear(self) -> None: ...
    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ...
 
 
def memo[**P, R](f: Callable[P, R]) -> HasCache[P, R]: ...

Тогда h.cache_clear() проходит, а h("x") при h(a: int) даёт ошибку — и она приходит с адресом: Argument 1 to "__call__" of "HasCache". Обратите внимание, что P.args и P.kwargs работают внутри протокола ровно так же, как в обычной функции: пара остаётся парой.

Фабрика: тут ошибаются почти все

Декоратор, умеющий обе формы — @trace и @trace(level=2), — пишут парой перегрузок, и выглядит она безупречно:

PYTHON
@overload
def trace[**P, R](f: Callable[P, R], /) -> Callable[P, R]: ...
@overload
def trace(*, level: int = ...) -> Callable[[Callable[..., object]], Callable[..., object]]: ...

Вывод на двух функциях, задекорированных разными формами (bench/typing/04_factory_broken.py):

04_factory_broken.py:30: note: Revealed type is "def (x: int) -> str"
04_factory_broken.py:31: note: Revealed type is "def (*Any, **Any) -> object"
04_factory_broken.py:32: error: Argument 1 to "a" has incompatible type "str"; expected "int"  [arg-type]

Первая строка — @trace, сигнатура цела. Вторая — @trace(level=2), сигнатуры больше нет. Третья — ошибка на a("bad"). А строкой ниже стоит b("bad"), ровно такой же неверный вызов, и он не подчёркнут вовсе.

Причина видна во второй перегрузке. Фабрика обязана вернуть один тип, а ParamSpec должен связываться заново на каждом применении декоратора. Один Callable[[Callable[..., object]], Callable[..., object]] этого не умеет — и многоточие внутри него делает ровно то, с чего начиналась статья.

Лечится тем, что фабрика возвращает не Callable, а протокол с обобщённым __call__ (bench/typing/05_factory_fixed.py):

PYTHON
class _Deco(Protocol):
    def __call__[**P, R](self, f: Callable[P, R], /) -> Callable[P, R]: ...
 
 
@overload
def trace[**P, R](f: Callable[P, R], /) -> Callable[P, R]: ...
@overload
def trace(*, level: int = ...) -> _Deco: ...

Параметры типа переехали с функции на сам __call__, и связываются они теперь в момент применения. Вывод:

05_factory_fixed.py:34: note: Revealed type is "def (x: int) -> str"
05_factory_fixed.py:35: note: Revealed type is "def (x: int) -> str"
05_factory_fixed.py:36: error: Argument 1 to "b" has incompatible type "str"; expected "int"  [arg-type]

Обе формы дают одну и ту же сигнатуру, и b("bad") наконец ловится.

Попутно — наблюдение, которое стоило нам самим одной правки. Реализация под @overload сначала стояла без аннотаций (def trace(f=None, *, level=1):). mypy на такую реализацию не смотрит вовсе; pyright выдавал по две ошибки Overloaded implementation is not consistent with signature of overload. Аннотация реализации убрала обе, ничего не изменив в выводах примера. Неаннотированная реализация перегрузки для mypy невидима — это стоит помнить и за пределами декораторов.

История версий

ВерсияИзменениеЧто это значит для кода
3.10PEP 612: появляются ParamSpec и Concatenate. До этого декоратор, сохраняющий сигнатуру, выразить было нечем — только callback-протокол с руками выписанными параметрами.
3.12PEP 695: синтаксис def deco[**P, R] вместо отдельного объявления P = ParamSpec("P"). Измерено: с 3.12 typing.ParamSpec — это буквально _typing.ParamSpec, тип из встроенного в интерпретатор модуля (typing.ParamSpec is _typing.ParamSpec даёт True, у _typing нет __file__). На 3.11 у _typing атрибута ParamSpec нет вовсе.
3.13PEP 696: у параметров типа появляются значения по умолчанию, в том числе у ParamSpec. Измерено: TypeVar(..., default=int) на 3.12 даёт TypeError, а def f[T = int]SyntaxError; на 3.13 работает и то и другое.
3.14PEP 649: аннотации вычисляются лениво. Практический подарок для декораторов — протокол, описывающий обёртку, можно объявить ниже декоратора, который его возвращает, без кавычек и без from __future__ import annotations. Измерено: на 3.13 такое определение падает NameError прямо на строке def, на 3.14 проходит, а annotationlib отдаёт ForwardRef.

Чем измерено

Числа этой статьи получены этими файлами. Каждый открывается прямо отсюда — вместе с записью прогона: чем проверяли, какой командой и что вышло дословно.

Проверяющих два: mypy 2.3.1 и pyright 1.1.413, оба под CPython 3.13.7. На всех семи файлах они сошлись — те же строки, те же количества, те же выводы. Это важно назвать: вывод любого инструмента здесь — наблюдение, а не источник. Первична спецификация системы типов, и там, где статья опирается на правило, а не на наблюдение, стоит цитата из неё или из PEP.

Матрица версий снята одним и тем же файлом на четырёх интерпретаторах: 3.11.15, 3.12.3, 3.13.7 и 3.14.7 — все четыре релизные сборки, не кандидаты. 3.11 здесь не для полноты: только на ней видно, что ParamSpec не всегда жил в C.

Времени в этой статье нет намеренно. Типизация на скорость не влияет, и любая таблица наносекунд здесь была бы таблицей про PEP 649, а не про ParamSpec.

Частые заблуждения

Утверждение

functools.wraps чинит типы

На самом деле

Он чинит интроспекцию, и только её. wraps ставит wrapper.__wrapped__ = wrapped (Lib/functools.py:62), а inspect.signature по этой ссылке прыгает, потому что объявлен с follow_wrapped=True (Lib/inspect.py:3373). Проверяющему это не говорит ничего: wraps выполняется при декорировании, а проверяющий до выполнения не доходит. Измерено на bench/typing/01_naive.py: оба заведомо неверных вызова проходят молча. wraps в этом файле нет — и в этом соль: он ничего бы не изменил.

Утверждение

Callable[..., R] означает «любые аргументы»

На самом деле

Означает «не проверять». Документация typing формулирует мягко («подойдёт вызываемый объект с любым произвольным списком параметров»), а PEP 612 прямо: an ellipsis in place of parameter types is specified to mean that we do no validation on arguments (многоточие вместо типов параметров, по спецификации, означает, что мы не выполняем никакой проверки аргументов). Измерено на bench/typing/01_naive.py: ноль ошибок, выход с кодом 0.

Утверждение

После декоратора цепочки вызовов ломаются: Self теряется

На самом деле

Не теряется, если декоратор объявлен через Concatenate[S, P]. Измерено на bench/typing/06_methods.py: у класса A с декорированными методами reveal_type(a.m) даёт def (x: int) -> str — получатель связан, — а reveal_type(a.chain()) даёт A. Оба проверяющих согласны.

Утверждение

Если @deco типизирован правильно, то и @deco(level=2) тоже

На самом деле

Это самое дорогое место темы. Измерено: при обычной паре перегрузок @trace даёт def (x: int) -> str, а @trace(level=2)def (*Any, **Any) -> object. Потеря молчаливая: a("bad") подчёркнуто, b("bad") строкой ниже — нет. Причина в том, что фабрика обязана вернуть один тип, а ParamSpec должен связываться заново на каждом применении. Лечится протоколом с обобщённым __call__.

Утверждение

ParamSpec с Concatenate умеет добавить декоратором именованный параметр

На самом деле

Не умеет, и это ограничение самого PEP 612, а не проверяющих. The semantics of Concatenate[X, Y, P] are that it represents the parameters represented by P with two positional-only parameters prepended (Семантика Concatenate[X, Y, P] такова, что он представляет параметры, представленные P, с двумя добавленными спереди позиционными-только параметрами). Для keyword-only в PEP есть отдельный подраздел с объяснением, почему так нельзя, и обещанием вернуться к вопросу «если будет достаточный спрос». В 3.14 не вернулись.

Проверка знаний

Вопрос 1 из 4

Декоратор объявлен как Callable[..., R] -> Callable[..., R], под ним функция takes_int_str(x: int, y: str). Сколько ошибок найдёт проверяющий на вызовах takes_int_str("B", 2) и takes_int_str()?

Источники и что читать дальше

9 ИСТОЧНИКОВ

  1. PEP 612 — Parameter Specification VariablesPEP. Марк Мендоза, Final, Python 3.10. Документ, которым `ParamSpec` и `Concatenate` вошли в язык, и единственное место, где прямо сказано, что означает многоточие: «an ellipsis in place of parameter types is specified to mean that we do no validation on arguments» (многоточие вместо типов параметров, по спецификации, означает, что мы не выполняем никакой проверки аргументов). Оттуда же постановка задачи: «Neither of these support forwarding the parameter types of one callable over to another callable, making it difficult to annotate function decorators» (Ни один из них не поддерживает проброс типов параметров одного вызываемого объекта в другой, что затрудняет аннотирование декораторов функций). Правило пары: «A ParamSpec captures both positional and keyword accessible parameters, but there unfortunately is no object in the runtime that captures both of these together» (ParamSpec захватывает как позиционно, так и по ключу доступные параметры, но, к сожалению, в среде выполнения нет объекта, который захватывал бы и то и другое вместе) и вывод: «we need to make sure that these special types are only brought into the world together, and are used together, so that our usage is valid for all possible partitions» (мы должны обеспечить, чтобы эти специальные типы появлялись на свет только вместе и использовались вместе — так, чтобы наше использование было корректным для всех возможных разбиений). Семантика соединения: «The semantics of Concatenate[X, Y, P] are that it represents the parameters represented by P with two positional-only parameters prepended» (Семантика Concatenate[X, Y, P] такова, что он представляет параметры, представленные P, с двумя добавленными спереди позиционными-только параметрами).https://peps.python.org/pep-0612/
  2. Спецификация системы типов — CallablesОфициальная документация. Первичнее поведения любого проверяющего. О многоточии: «The Callable special form supports the use of `...` in place of the list of parameter types. This is a gradual form indicating that the type is consistent with any input signature» (Специальная форма Callable поддерживает использование `...` вместо списка типов параметров. Это градуальная форма, указывающая, что данный тип совместим с любой входной сигнатурой). Здесь же, что многоточие сочетается с `Concatenate`: «A `...` can also be used with Concatenate» (`...` можно также использовать вместе с Concatenate).https://typing.python.org/en/latest/spec/callables.html
  3. Спецификация системы типов — GenericsОфициальная документация. Раздел про `ParamSpec` открывается пометкой о происхождении — «(Originally specified by PEP 612.)» ((Изначально специфицировано в PEP 612.)) — и дословно воспроизводит формулировки PEP: и правило совместного использования `P.args` / `P.kwargs`, и «Placing keyword-only parameters between the *args and **kwargs is forbidden» (Размещение keyword-only параметров между *args и **kwargs запрещено). То есть оба ключевых утверждения этой статьи имеют нормативную опору, а не только поведение инструмента.https://typing.python.org/en/latest/spec/generics.html
  4. PEP 695 — Type Parameter SyntaxPEP. Final, Python 3.12. Синтаксис, которым в статье записаны все декораторы: «The syntax adds support for a comma-delimited list of type parameters in square brackets after the name of the class, function, or type alias» (Синтаксис добавляет поддержку списка параметров типа, разделённого запятыми, в квадратных скобках после имени класса, функции или псевдонима типа). Двойная звёздочка для `ParamSpec` — часть грамматики: `type_param: | a=NAME b=[type_param_bound] | '*' a=NAME | '**' a=NAME`.https://peps.python.org/pep-0695/
  5. PEP 696 — Type Defaults for Type ParametersPEP. Final, Python 3.13. «This PEP introduces the concept of type defaults for type parameters, including TypeVar, ParamSpec, and TypeVarTuple, which act as defaults for type parameters for which no type is specified» (Этот PEP вводит понятие значений по умолчанию для параметров типа, включая TypeVar, ParamSpec и TypeVarTuple, которые выступают значениями по умолчанию для тех параметров типа, для которых тип не указан). В статье используется как граница версии: на 3.12 синтаксис `def f[T = int]` даёт SyntaxError, а `TypeVar(..., default=int)` — TypeError; на 3.13 работает и то и другое.https://peps.python.org/pep-0696/
  6. PEP 649 — Deferred Evaluation Of Annotations Using DescriptorsPEP. Final, Python 3.14. Механизм: «It adds a new internal mechanism for lazily computing annotations on demand, via a new object method called `__annotate__`» (Он добавляет новый внутренний механизм для ленивого вычисления аннотаций по требованию — через новый метод объекта, называемый `__annotate__`) и следствие: «This mechanism delays the evaluation of annotations expressions until the annotations are examined, which solves many circular reference problems» (Этот механизм откладывает вычисление выражений аннотаций до момента, когда аннотации будут запрошены, что решает многие проблемы циклических ссылок). Именно отсюда возможность объявить протокол ниже декоратора, который его возвращает.https://peps.python.org/pep-0649/
  7. Lib/functools.py — update_wrapperИсходный код CPython. Строка, из-за которой рантайм остаётся честным: `wrapper.__wrapped__ = wrapped` (`Lib/functools.py:62` на 3.13.7), и комментарий рядом объясняет, почему она последняя: «Issue #17482: set __wrapped__ last so we don't inadvertently copy it from the wrapped function when updating __dict__» (Issue #17482: ставим __wrapped__ последним, чтобы ненароком не скопировать его из обёрнутой функции при обновлении __dict__). Список копируемых полей — `WRAPPER_ASSIGNMENTS` на строке 33.https://github.com/python/cpython/blob/v3.13.7/Lib/functools.py
  8. Lib/inspect.py — signature и unwrapИсходный код CPython. Умолчание, из-за которого `inspect.signature` показывает не ту функцию, которую вызовут: `def signature(obj, *, follow_wrapped=True, ...)` (`Lib/inspect.py:3373` на 3.13.7). Прыжок по цепочке делает `unwrap` (строка 764). Проверено на 3.12.3, 3.13.7 и 3.14.7: с умолчанием видна исходная сигнатура, с `follow_wrapped=False` — `(*args, **kwargs)`.https://github.com/python/cpython/blob/v3.13.7/Lib/inspect.py
  9. typing — Support for type hintsОфициальная документация. Формулировка документации про многоточие мягче, чем в PEP: «If a literal ellipsis `...` is given as the argument list, it indicates that a callable with any arbitrary parameter list would be acceptable» (Если в качестве списка аргументов указано литеральное многоточие `...`, это означает, что подойдёт вызываемый объект с любым произвольным списком параметров). Отдельного предложения о том, что аргументы при этом не проверяются, на этой странице нет — оно есть в PEP 612, и разница между «подойдёт любой» и «не проверяем» как раз и есть предмет первого раздела статьи.https://docs.python.org/3/library/typing.html