Python generics и TypeVar — обобщённые типы

18 сентября 2026
Автор

Антон Ларичев

Что такое обобщённые типы и зачем они нужны

Представьте функцию, которая возвращает первый элемент списка. Без обобщённых типов её сигнатура выглядит так:

from typing import Any

def first(items: list[Any]) -> Any:
    return items[0]

Проблема очевидна: если передать list[int], тип возвращаемого значения всё равно будет Any. Статический анализатор теряет информацию о типе, и весь смысл аннотаций пропадает.

Обобщённые типы (generics) решают эту задачу: позволяют писать код, который работает с разными типами данных, сохраняя при этом полную информацию о типах для проверки.

from typing import TypeVar

T = TypeVar('T')

def first(items: list[T]) -> T:
    return items[0]

result = first([1, 2, 3])   # тип: int
name = first(['a', 'b'])    # тип: str

Теперь анализатор знает: если передать list[int], функция вернёт int.

Курс по теме

Основы Python — курс

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

TypeVar — переменная типа

TypeVar создаёт «переменную», которая при каждом вызове функции подставляется конкретным типом. Главное правило: имя переменной должно совпадать со строкой, переданной в конструктор.

from typing import TypeVar

T = TypeVar('T')        # правильно
K = TypeVar('K')        # правильно
Value = TypeVar('Value') # правильно

# Неправильно — имя не совпадает со строкой:
# X = TypeVar('Y')

Несколько TypeVar в одной функции

Каждый TypeVar — независимая переменная типа:

from typing import TypeVar

K = TypeVar('K')
V = TypeVar('V')

def zip_to_dict(keys: list[K], values: list[V]) -> dict[K, V]:
    return dict(zip(keys, values))

result = zip_to_dict(['a', 'b'], [1, 2])
# тип result: dict[str, int]

Ограничения TypeVar

Параметр constraints — фиксированный набор типов

Иногда нужно разрешить только конкретные типы. Для этого используют constraints:

from typing import TypeVar

AnyStr = TypeVar('AnyStr', str, bytes)

def encode(value: AnyStr) -> AnyStr:
    if isinstance(value, str):
        return value.upper()  # type: ignore[return-value]
    return value.upper()

result_str = encode('hello')   # тип: str
result_bytes = encode(b'hi')   # тип: bytes

# encode(123)  — ошибка типа: int не входит в constraints

Важный нюанс: при использовании constraints тип фиксируется как один из указанных, а не как их объединение. То есть внутри функции нельзя использовать AnyStr как одновременно str | bytes.

Параметр bound — верхняя граница типа

bound означает «этот тип или любой его подтип»:

from typing import TypeVar
from datetime import date, datetime

DateLike = TypeVar('DateLike', bound=date)

def format_date(value: DateLike) -> str:
    return value.strftime('%Y-%m-%d')

format_date(date(2024, 1, 15))      # работает: date
format_date(datetime(2024, 1, 15))  # работает: datetime — подтип date
# format_date('2024-01-15')         # ошибка: str не является подтипом date

Разница между constraints и bound:

from typing import TypeVar

# bound: принимает date и любые его подклассы
DateType = TypeVar('DateType', bound=date)

# constraints: принимает ТОЛЬКО date или ТОЛЬКО datetime, ничего другого
ExactDate = TypeVar('ExactDate', date, datetime)

Generic-классы

Для создания обобщённых классов нужно унаследоваться от Generic[T]:

from typing import TypeVar, Generic

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

    def peek(self) -> T:
        return self._items[-1]

    def is_empty(self) -> bool:
        return len(self._items) == 0

# Использование
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
value = int_stack.pop()  # тип: int

str_stack: Stack[str] = Stack()
str_stack.push('hello')
text = str_stack.pop()  # тип: str

# int_stack.push('oops')  # ошибка типа

Generic с несколькими параметрами

from typing import TypeVar, Generic

K = TypeVar('K')
V = TypeVar('V')

class Pair(Generic[K, V]):
    def __init__(self, key: K, value: V) -> None:
        self.key = key
        self.value = value

    def swap(self) -> 'Pair[V, K]':
        return Pair(self.value, self.key)

    def __repr__(self) -> str:
        return f'Pair({self.key!r}, {self.value!r})'

pair = Pair('name', 42)
swapped = pair.swap()  # тип: Pair[int, str]
print(swapped)         # Pair(42, 'name')

Обобщённые типы в Python 3.12+

С версии Python 3.12 появился новый синтаксис через квадратные скобки, без импорта TypeVar и Generic:

# Python 3.12+
def first[T](items: list[T]) -> T:
    return items[0]

class Stack[T]:
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

Этот синтаксис чище, но если нужна поддержка Python 3.9–3.11, используйте классический подход с TypeVar.

Protocol и обобщённые интерфейсы

Protocol позволяет описывать структурные типы (duck typing). В сочетании с Generic это мощный инструмент:

from typing import TypeVar, Protocol, runtime_checkable

T = TypeVar('T')

@runtime_checkable
class Comparable(Protocol):
    def __lt__(self, other: 'Comparable') -> bool: ...
    def __le__(self, other: 'Comparable') -> bool: ...

C = TypeVar('C', bound=Comparable)

def maximum(items: list[C]) -> C:
    if not items:
        raise ValueError('Список не может быть пустым')
    result = items[0]
    for item in items[1:]:
        if result < item:
            result = item
    return result

print(maximum([3, 1, 4, 1, 5, 9]))      # 9, тип: int
print(maximum(['banana', 'apple', 'cherry']))  # cherry, тип: str

Generic Protocol

from typing import TypeVar, Generic, Protocol

T_co = TypeVar('T_co', covariant=True)

class Container(Protocol[T_co]):
    def get(self) -> T_co: ...
    def __len__(self) -> int: ...

class Box(Generic[T_co]):
    def __init__(self, value: T_co) -> None:
        self._value = value

    def get(self) -> T_co:
        return self._value

    def __len__(self) -> int:
        return 1

def process(container: Container[int]) -> int:
    return container.get() * len(container)

box: Box[int] = Box(42)
print(process(box))  # 42

Вариантность: covariant и contravariant

Вариантность описывает, как Generic-тип ведёт себя при подтипизации.

from typing import TypeVar

# Инвариантный (по умолчанию) — точное совпадение типа
T = TypeVar('T')

# Ковариантный — принимает тип и его подтипы
T_co = TypeVar('T_co', covariant=True)

# Контравариантный — принимает тип и его супертипы
T_contra = TypeVar('T_contra', contravariant=True)

Практический пример с ковариантностью:

from typing import TypeVar, Generic

T_co = TypeVar('T_co', covariant=True)

class ReadOnlyList(Generic[T_co]):
    def __init__(self, items: list[T_co]) -> None:
        self._items = list(items)

    def get(self, index: int) -> T_co:
        return self._items[index]

    def __len__(self) -> int:
        return len(self._items)

class Animal:
    name: str
    def __init__(self, name: str) -> None:
        self.name = name

class Dog(Animal):
    def bark(self) -> str:
        return 'Woof!'

def show_animals(animals: ReadOnlyList[Animal]) -> None:
    for i in range(len(animals)):
        print(animals.get(i).name)

dogs: ReadOnlyList[Dog] = ReadOnlyList([Dog('Rex'), Dog('Buddy')])
show_animals(dogs)  # работает благодаря ковариантности

ParamSpec — обобщение по сигнатуре функции

ParamSpec позволяет сохранять типы параметров при декорировании функций:

from typing import TypeVar, Callable
from typing import ParamSpec
import functools
import time

P = ParamSpec('P')
R = TypeVar('R')

def timer(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f'{func.__name__} выполнилась за {elapsed:.4f}с')
        return result
    return wrapper

@timer
def compute(x: int, y: int) -> int:
    return x + y

result = compute(1, 2)  # тип result: int — тип не теряется

Без ParamSpec декоратор терял бы информацию о типах параметров.

Практический пример: обобщённый репозиторий

Классический паттерн в проектах — Generic-репозиторий:

from typing import TypeVar, Generic, Optional
from dataclasses import dataclass, field

@dataclass
class User:
    id: int
    name: str
    email: str

@dataclass
class Product:
    id: int
    title: str
    price: float

T = TypeVar('T')

class InMemoryRepository(Generic[T]):
    def __init__(self) -> None:
        self._storage: dict[int, T] = {}
        self._next_id: int = 1

    def save(self, item: T) -> int:
        item_id = self._next_id
        self._storage[item_id] = item
        self._next_id += 1
        return item_id

    def find_by_id(self, item_id: int) -> Optional[T]:
        return self._storage.get(item_id)

    def find_all(self) -> list[T]:
        return list(self._storage.values())

    def delete(self, item_id: int) -> bool:
        if item_id in self._storage:
            del self._storage[item_id]
            return True
        return False

# Использование — анализатор знает точные типы
user_repo: InMemoryRepository[User] = InMemoryRepository()
product_repo: InMemoryRepository[Product] = InMemoryRepository()

user_id = user_repo.save(User(0, 'Антон', 'anton@example.com'))
user = user_repo.find_by_id(user_id)  # тип: Optional[User]

if user:
    print(user.email)  # автодополнение работает корректно

prod_id = product_repo.save(Product(0, 'Курс Python', 4990.0))
product = product_repo.find_by_id(prod_id)  # тип: Optional[Product]

if product:
    print(product.price)  # тип: float

TypeVarTuple — обобщение по кортежу типов

Добавлен в Python 3.11, позволяет параметризовать переменное число типов:

from typing import TypeVarTuple, Unpack

Ts = TypeVarTuple('Ts')

def broadcast(
    func: 'Callable[[Unpack[Ts]], None]',
    *args: Unpack[Ts]
) -> None:
    func(*args)

Чаще встречается при работе с библиотеками вроде NumPy для типизации форм тензоров.

Распространённые ошибки

Использование одного TypeVar для несвязанных параметров

from typing import TypeVar

T = TypeVar('T')

# Неправильно: T должен быть одним типом и для входа, и для выхода
# Это означает: если передать int, вернётся int, а не произвольный тип
def bad_convert(value: T, target_type: type[T]) -> T:
    return target_type(value)  # type: ignore

# Правильно: два разных TypeVar
Source = TypeVar('Source')
Target = TypeVar('Target')

def convert(value: Source, converter: 'Callable[[Source], Target]') -> Target:
    return converter(value)

result = convert('42', int)  # тип result: int

Избыточный Generic при наследовании

from typing import TypeVar, Generic

T = TypeVar('T')

class Base(Generic[T]):
    def get(self) -> T: ...

# Неправильно — дублирование Generic[T]
class Child(Base[T], Generic[T]):
    pass

# Правильно — Generic[T] наследуется через Base[T]
class Child(Base[T]):
    pass

Итог

Обобщённые типы в Python позволяют писать переиспользуемый код без потери информации о типах:

  • TypeVar создаёт переменную типа для функций и классов
  • bound ограничивает тип снизу — принимает указанный тип и его подтипы
  • constraints фиксирует конкретный набор допустимых типов
  • Generic[T] превращает класс в обобщённый контейнер
  • Protocol + Generic описывает структурные обобщённые интерфейсы
  • ParamSpec сохраняет сигнатуру функций при декорировании
  • Python 3.12+ предлагает более лаконичный синтаксис через [T]

Правильное использование generics делает API библиотек и модулей самодокументируемым: пользователь сразу видит, что возвращает функция, без необходимости смотреть в исходники.

Чтобы глубже освоить систему типов Python и научиться писать профессиональный Python-код, записывайтесь на курс — Python-разработчик на PurpleSchool.

Стрелочка влевоPython list comprehensions — списочные включения

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий