2025, Dec 15 18:02
Аннотации tk.Event в Tkinter: решения для Python и Pylance
Почему Pylance требует tk.Event как дженерик, а Python падает с TypeError? Разбираем причину и даем два решения аннотаций Tkinter, совместимых с рантаймом.
Аннотирование обратных вызовов GUI в Tkinter кажется простым — до тех пор, пока рантайм не возражает. Классический случай: Pylance просит параметризовать tk.Event как обобщённый тип, но интерпретатор при этом выбрасывает TypeError. Этот материал объясняет, откуда берётся несоответствие, почему оно возникает и как писать аннотации, чтобы были довольны и проверяющий типы, и сам Python.
Как воспроизвести проблему
Ниже — минимальный пример, где клик по кнопке привязывается к обработчику. Обработчик аннотирован типом tk.Event, и Pylance помечает это предупреждением: “Expected type arguments for generic class "Event"”.
import tkinter as tk
class Demo:
def __init__(self, root: tk.Tk) -> None:
host: tk.Frame = tk.Frame(master=root)
host.grid()
trigger: tk.Button = tk.Button(master=host, text="Button")
trigger.bind("<ButtonPress-1>", self.on_fire)
def on_fire(self, evt: tk.Event) -> None:
pressed_btn: tk.Button = evt.widget
Попытка угодить типизатору и использовать индексируемый дженерик убирает предупреждение, но приводит к падению во время выполнения:
def on_fire(self, evt: tk.Event[tk.Button]) -> None:
...
И Python отвечает так:
TypeError: type 'Event' is not subscriptable
Что на самом деле происходит
Подсказки типов в Python оцениваются во время выполнения. Заглушки (.pyi) могут объявлять типы обобщёнными для статического анализа, даже если соответствующие объекты рантайма не являются дженериками. Ровно это и создаёт рассинхрон: типизатор ожидает запись наподобие tk.Event[tk.Button], а реальный класс tk.Event в рантайме не поддерживает индексацию, поэтому при попытке немедленно вычислить такую аннотацию Python поднимает TypeError.
Немедленная оценка аннотаций давно известна побочными эффектами: NameError из‑за порядка определения, циклические импорты и лишние накладные расходы. Чтобы это смягчить, PEP 563 ввёл «строковые» аннотации, а позже в Python появился модульный переключатель, заставляющий трактовать все аннотации как строки. Оба подхода решают случай с Tkinter Event, потому что не дают рантайму тут же пытаться индексировать tk.Event.
Есть и смежная ремарка: пока такие ситуации можно рассматривать как «дженерик только в заглушках» и либо брать аннотацию в кавычки, либо использовать модульный переключатель. Контекст — здесь: https://github.com/python/cpython/issues/123341.
Два практичных решения
Идея обоих решений одинакова: отложить вычисление аннотации в рантайме, чтобы Python ни при импорте, ни при определении функции не пытался индексировать tk.Event.
Первый подход: взять в кавычки только проблемную аннотацию.
import tkinter as tk
class Demo:
def __init__(self, root: tk.Tk) -> None:
host: tk.Frame = tk.Frame(master=root)
host.grid()
trigger: tk.Button = tk.Button(master=host, text="Button")
trigger.bind("<ButtonPress-1>", self.on_fire)
def on_fire(self, evt: 'tk.Event[tk.Button]') -> None:
pressed_btn: tk.Button = evt.widget
Второй подход: перевести весь модуль на отложенные аннотации. Тогда можно оставить обычный синтаксис индексации без кавычек.
from __future__ import annotations
import tkinter as tk
class Demo:
def __init__(self, root: tk.Tk) -> None:
host: tk.Frame = tk.Frame(master=root)
host.grid()
trigger: tk.Button = tk.Button(master=host, text="Button")
trigger.bind("<ButtonPress-1>", self.on_fire)
def on_fire(self, evt: tk.Event[tk.Button]) -> None:
pressed_btn: tk.Button = evt.widget
О Python 3.14 и отложенной оценке
С PEP 649 (Deferred Evaluation Of Annotations Using Descriptors) в Python 3.14 аннотации больше не вычисляются немедленно. Формально некорректные для рантайма аннотации становятся допустимыми, пока к ним не обратятся. Оценка происходит при первом доступе и зависит от способа получения аннотаций и используемых глобалов. В этом сценарии, если вы не обращаетесь к аннотациям во время выполнения, про их оценку можно вовсе не думать. Для статического анализатора поведение будет таким же, как и при «кавычках» или модульном переключателе, описанных выше.
Почему это важно
Свести проверку типов с реальным исполнением — критично для проектов на Python с активным использованием инструментов. В GUI это особенно заметно: сигнатуры обработчиков часто опираются на типы фреймворка, которые могут быть обобщёнными в заглушках, но не в «живых» объектах. Понимание того, как и когда вычисляются аннотации, помогает избежать скрытых падений при импорте или регистрации обработчика и держит редакторы вроде VS Code с Pylance в согласии с интерпретатором.
Выводы
Если типизатор требует указать параметры обобщённого типа для объекта фреймворка, а Python отклоняет это в рантайме, скорее всего перед вами «дженерик только в заглушках». Не скатывайтесь в Any и не подавляйте диагностику. Вместо этого отложите вычисление аннотации: либо возьмите тип в кавычки на месте, либо включите from __future__ import annotations для всего файла. На современном Python с отложенной оценкой действует тот же принцип: пока вы не заставляете аннотации вычисляться во время выполнения, они безопасны, а типизация остаётся эффективной.