Как правильно аннотировать типы полей в Python, если эти поля имеют разные типы в подклассах при наследовании?

Я не эксперт в программировании, просто любитель, и меня начали путать разные советы от ИИ, поэтому прошу совета у знатоков.
  • 17 июля 2026 г. 16:00
Ответы на вопрос 2
Коротко — вариантов немного, и лучший выбор зависит от семантики поля. Главное понятие — Liskov Substitution Principle: если кто‑то использует объект через тип базового класса, то поведение/тип поля в подклассе не должен ломать этот код. Ниже — практичные подходы с примерами и рекомендациями.

1) Поле имеет одну «логическую» роль, но разные подклассы дают разные конкретные типы → сделайте класс обобщённым (Generic)
- Когда поле предназначено быть тем же самым «контрактом», но конкретный тип меняется, параметризуйте класс.
Пример:
  from typing import Generic, TypeVar
  T = TypeVar('T')
  class Base(Generic[T]):
      value: T
  class IntSub(Base[int]):
      value: int
  class StrSub(Base[str]):
      value: str

Плюсы: чётко выражена разная типизация, type‑checkers (mypy/pyright) понимают, что Base[int] и Base[str] — разные типы.

2) Поле объявлено в базовом классе и изменяется в подклассах, но это поле мутируемое или используется полиморфно → не сужайте тип в подклассе
Если базовый класс объявляет value: str, а вы в подклассе делаете value: int — это обычно ошибка, потому что код, работающий с Base, может ожидать str и поломаться. Решения:
- Задать в базовом классе более широкий тип (Union) или общий суперкласс:
    value: Union[str, int]
- Или убрать поле из базового класса и объявлять его только в подклассах, если семантика различается.

3) Поле только для чтения (иммутабельное) → можно определить абстрактное свойство в базе и в подклассах возвращать более узкий тип
Пример:
  from typing import Protocol, runtime_checkable
  class Base(Protocol):
      @property
      def value(self) -> int: ...
  # подкласс может иметь value: int

Или через Generic с ковариантным TypeVar, если нужно:
  from typing import Generic, TypeVar
  T_co = TypeVar('T_co', covariant=True)
  class Base(Generic[T_co]):
      @property
      def value(self) -> T_co: ...

Ковариантность безопасна для выходных (read‑only) значений.

4) Если вы не уверены → Any или cast (как временное решение)
  from typing import Any
  class Base:
      value: Any
Это снимает проверки типов, но теряется статическая гарантия. Можно использовать typing.cast, чтобы локально подавлять предупреждение.

5) Практические рекомендации
- Подумайте: поле должно быть одинаковым «контрактом» у всех подклассов? Тогда Generic или общий суперттип.
- Если поле у разных подклассов — разные по смыслу вещи, не заявляйте его в базовом классе.
- Если поле изменяется только для чтения — используйте свойство/абстрактное свойство и ковариантность.
- Не переопределяйте тип поля в подклассе на более узкий, если поле предполагается мутировать или использовать через базовый тип — это приведёт к предупреждениям и возможным ошибкам времени выполнения.

Если хотите, пришлите пример вашего кода — подскажу конкретно, какой подход лучше и покажу, как правильно аннотировать.
линтер прав: list инвариантен, сужение list[ParFile] до list[ChildFile] в наследнике ломает контракт родителя. <br/> <br/> сделай сам ParFolder generic-классом, создание файла вынеси в отдельный метод: <br/> <br/> <pre><code>class ParFolder(Generic[FileT]):
    def __init__(self, path):
        self.folders: list[Self] = []
        self.files: list[FileT] = []
    def make_file(self, path) -&gt; FileT: return ParFile(path)

class ChildFolder(ParFolder[ChildFile]):
    def make_file(self, path) -&gt; ChildFile: return ChildFile(path)</code></pre> <br/> <br/> ChildFolder буквально наследует ParFolder, files параметризован ChildFile, folders через Self сам подставляет ChildFolder. mypy это реально ловит. <br/> <br/> p.s. Self с 3.11, для старых версий typing_extensions.Self
Похожие вопросы