Типизация данных в Python с помощью typing

Раздел: Основы Python -> Типы данных

Основы работы с модулем typing в Python

Модуль typing предоставляет инструменты для описания типов данных в Python. Аннотации не изменяют работу программы, но помогают статическим анализаторам и другим разработчикам понимать ожидаемые значения.

Как аннотировать переменную и функцию в простейшем случае?

Основной подход к типизации в Python заключается в записи аннотаций после двоеточия для параметров и после стрелки для результата.

from typing import List

def average(values: List[float]) -> float:
    return sum(values) / len(values)

scores = [90.5, 100.0, 78.0]
print(average(scores))

значение bool python (значение bool в python)

89.5

длина переменной python (длина числа и переменной в python)

Параметр values описан как список чисел с плавающей точкой. Функция возвращает число с плавающей точкой. Если передать список строк, статический анализатор укажет на несоответствие.

Вариант 1: Optional для значений с возможным None

Как указать, что параметр может быть пустым?

Тип Optional[int] означает int или None. Такой вариант применяется для аргументов со значением по умолчанию None и для результатов, которые могут отсутствовать.

from typing import Optional

def parse_number(text: Optional[str]) -> Optional[int]:
    if text is None:
        return None
    try:
        return int(text)
    except ValueError:
        return None

print(parse_number('42'))
print(parse_number(None))
print(parse_number('abc'))

Python максимальное целое число (максимальное целое число в python)

42
None
None

Python list dict str str (список словарей со строковыми ключами в python)

Ветвление с проверкой None позволяет сузить тип до str внутри функции.

Частая ошибка: попытка выполнить арифметические операции с результатом Optional[int] без проверки None. Анализатор сообщит о возможной ошибке.

Вариант 2: Union для нескольких разных типов

Как передать в функцию значение одного из нескольких типов?

Union[int, str] принимает int или str. В Python 3.10 поддерживается запись int | str.

from typing import Union

def render(value: Union[int, str]) -> str:
    if isinstance(value, int):
        return 'Число: ' + str(value)
    return 'Строка: ' + value

print(render(5))
print(render('пять'))

оператор float python (оператор float в python)

Число: 5
Строка: пять

Type 0 python (тип 0 в python)

Проверка isinstance сужает объединенный тип до конкретного варианта внутри каждой ветки.

Если не выполнить проверку типа, код может вызвать метод, доступный только одному из вариантов.

Вариант 3: Аннотации для коллекций

Как описать список, словарь, кортеж или множество с конкретными типами элементов?

Для этой задачи служат List, Dict, Tuple, Set из модуля typing. В Python 3.9 и новее доступны встроенные list[int], dict[str, int], tuple[int, int], set[str].

from typing import List, Dict, Tuple, Set

def build_config() -> Dict[str, List[int]]:
    config = {
        'ports': [8000, 8001],
        'limits': [10, 20, 30],
    }
    return config

def pair_scores() -> Tuple[str, int]:
    return ('Артем', 5)

def unique_codes() -> Set[int]:
    return {101, 102, 103}

print(build_config())
print(pair_scores())
print(unique_codes())

S type python (тип s в python)

{'ports': [8000, 8001], 'limits': [10, 20, 30]}
('Артем', 5)
{101, 102, 103}

тип элемента python (тип элемента в python)

Вложенные конструкции, такие как Dict[str, List[int]], указывают типы для всех уровней.

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

Вариант 4: Callable для передачи функций

Как описать функцию, которую планируется передать в другую функцию?

Callable[[int, int], int] означает вызываемый объект с двумя аргументами int и возвращаемым значением int. Случай с разной сигнатурой записывается как Callable[..., int].

from typing import Callable

def perform_operation(a: int, b: int, operation: Callable[[int, int], int]) -> int:
    return operation(a, b)

def add(x: int, y: int) -> int:
    return x + y

def multiply(x: int, y: int) -> int:
    return x * y

print(perform_operation(6, 7, add))
print(perform_operation(6, 7, multiply))

Code typing python (примеры кода с модулем typing в python)

13
42

целое положительное число python (целое положительное число)

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

Ошибка возникает, если передать функцию с неправильным количеством параметров или несовместимыми типами.

Вариант 5: TypeVar для обобщенных функций

Как сохранить связь между типом входного списка и типом возвращаемого элемента?

TypeVar объявляет переменную типа. Функция get_first(items: List[T]) -> T гарантирует, что тип результата совпадает с типом элементов списка.

from typing import List, TypeVar

T = TypeVar('T')

def get_first(items: List[T]) -> T:
    return items[0]

print(get_first([1, 2, 3]))
print(get_first(['a', 'b', 'c']))

Int python значения (целочисленные значения (int) в python)

1
a

Set str python (множество из строки в python)

В первом вызове T принимает значение int, во втором str.

Вариант 6: Generic классы

Как построить класс, который работает с разными типами, но сохраняет информацию о типе?

Наследование от Generic[T] превращает класс в обобщенный. Атрибуты и методы могут использовать T как обычный тип.

from typing import Generic, TypeVar

T = TypeVar('T')

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

    def take(self) -> T:
        return self.value

number_box = Box(100)
text_box = Box('hello')

print(number_box.take() + 10)
print(text_box.take().upper())

Python словарь объектов (словарь объектов в python)

110
HELLO

Str string python (функция str и строковый тип в python)

При создании number_box тип T автоматически выводится как int, для text_box как str.

Вариант 7: Protocol для структурной типизации

Как описать необходимые методы и атрибуты без создания общего базового класса?

Protocol определяет набор методов. Любой класс, который содержит эти методы, считается подходящим, даже без наследования от Protocol.

from typing import Protocol

class Shape(Protocol):
    def area(self) -> float:
        ...

class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

class Circle:
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

def print_area(shape: Shape) -> None:
    print(shape.area())

print_area(Rectangle(2, 3))
print_area(Circle(1))
6.0
3.14159

Protocol удобен при работе с разными библиотеками, когда не хочется заставлять классы наследовать общий интерфейс.

Вариант 8: Literal и Final

Как ограничить допустимые значения константы и запретить ее переназначение?

Literal['auto', 'manual'] допускает только перечисленные строки. Final[int] указывает, что значение не должно меняться после инициализации.

from typing import Final, Literal

TOTAL_STUDENTS: Final[int] = 30
mode: Literal['auto', 'manual'] = 'auto'

def set_mode(new_mode: Literal['auto', 'manual']) -> None:
    global mode
    mode = new_mode

print(TOTAL_STUDENTS)
print(mode)
set_mode('manual')
print(mode)
30
auto
manual

Подобные аннотации полезны для конфигураций, где допустимы строго определенные значения.

Изменение TOTAL_STUDENTS не приведет к ошибке во время выполнения, но статический анализатор зафиксирует нарушение.

Вариант 9: TypedDict для структурированных словарей

Как описать словарь с фиксированным набором ключей и типами значений?

TypedDict задает схему словаря. В отличие от Dict[str, int], здесь перечисляются конкретные ключи.

from typing import TypedDict

class UserProfile(TypedDict):
    name: str
    age: int
    email: str

profile: UserProfile = {
    'name': 'Мария',
    'age': 27,
    'email': 'maria@example.com',
}

print(profile['name'])
print(profile['age'])
Мария
27

Если добавить ключ, отсутствующий в схеме, статический анализатор сообщит об ошибке.

Вариант 10: NewType для отдельных типов

Как различить числа с разным смыслом, например идентификатор пользователя и количество?

NewType создает производный тип. Функция с параметром UserId не примет обычный int, хотя во время выполнения user_id остается целым числом.

from typing import NewType

UserId = NewType('UserId', int)

def get_user(identifier: UserId) -> str:
    return 'user_' + str(identifier)

user_id = UserId(42)
print(get_user(user_id))
user_42

NewType применяется, когда нужно различать на уровне типов разные сущности, хранящиеся как int.

Типичные ошибки при использовании typing

Аннотации типов не выполняются в рантайме. Для проверки соответствия используют mypy или другой статический анализатор.

Ошибка 1: пропущен импорт. Например, использование Optional без строки from typing import Optional приводит к исключению NameError.

Ошибка 2: устаревший синтаксис. Для Python 3.8 нужен typing.List, а для Python 3.9 и новее допустимо list[int].

Ошибка 3: слишком частое использование Any. Any отключает проверку типа и скрывает настоящие проблемы.

Ошибка 4: несовпадение структуры в TypedDict. Словарь должен содержать ровно те ключи и с теми типами, что объявлены.

Дополнительные примеры работы с typing

TypeVar с ограничением bound

Bound ограничивает возможные типы подклассами указанного класса.

Пример
from typing import TypeVar

class Shape:
    def area(self) -> float:
        return 0.0

class Square(Shape):
    def __init__(self, side: float) -> None:
        self.side = side

    def area(self) -> float:
        return self.side * self.side

S = TypeVar('S', bound=Shape)

def double_area(shape: S) -> float:
    return shape.area() * 2

square = Square(4)
print(double_area(square))
32.0

Переменная S может быть только Shape или его наследником.

Перегрузка функций с помощью overload

Overload описывает несколько сигнатур для одной функции. Реализация записывается в последнем определении без декоратора.

Пример
from typing import overload

@overload
def parse(item: int) -> int: ...

@overload
def parse(item: str) -> str: ...

def parse(item):
    if isinstance(item, str):
        return item.lower()
    return int(item) * 2

print(parse(21))
print(parse('ABC'))
42
abc

Такой подход удобен для библиотек, где функция ведет себя по-разному для разных типов аргументов.

Protocol с проверкой на этапе выполнения

Декоратор runtime_checkable включает поддержку isinstance для протоколов.

Пример
from typing import Protocol, runtime_checkable

@runtime_checkable
class Greeter(Protocol):
    def greet(self) -> str: ...

class Person:
    def greet(self) -> str:
        return 'Привет'

print(isinstance(Person(), Greeter))
True

Обычный Protocol без runtime_checkable не поддерживает isinstance.

ParamSpec для декораторов с сохранением сигнатуры

ParamSpec позволяет описать декоратор, который передает оригинальные параметры без их потери.

Пример
from typing import Callable, ParamSpec, TypeVar

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

def add_logging(func: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print('Calling')
        return func(*args, **kwargs)
    return wrapper

@add_logging
def add(a: int, b: int) -> int:
    return a + b

print(add(2, 3))
Calling
5

Без ParamSpec декоратор получил бы тип Callable[..., Any] и потерял связь между аргументами и результатом.

Вложенные TypedDict структуры

TypedDict можно использовать внутри других TypedDict.

Пример
from typing import TypedDict

class Address(TypedDict):
    city: str
    zip_code: str

class PersonData(TypedDict):
    name: str
    address: Address

person: PersonData = {
    'name': 'Иван',
    'address': {
        'city': 'Москва',
        'zip_code': '101000',
    },
}

print(person['address']['city'])
Москва

Структура вложенных словарей получает полную аннотацию.

Псевдонимы типов

Для сложных аннотаций удобно вводить псевдоним.

Пример
from typing import Dict, List, Tuple, TypeAlias

Point: TypeAlias = Tuple[float, float]
Path: TypeAlias = List[Point]
RouteMap: TypeAlias = Dict[str, Path]

route: RouteMap = {
    'main': [(0.0, 0.0), (1.5, 2.0)],
}

print(route)
{'main': [(0.0, 0.0), (1.5, 2.0)]}

Псевдоним делает аннотации более читаемыми.

TypeGuard для сужения типа

TypeGuard указывает, что функция является проверкой типа для конкретного значения.

Пример
from typing import Any, TypeGuard, List

def is_str_list(value: List[Any]) -> TypeGuard[List[str]]:
    return all(isinstance(item, str) for item in value)

data = ['a', 'b', 'c']

if is_str_list(data):
    print(' '.join(data).upper())
A B C

Внутри ветки if анализатор считает data списком строк.

Примеры кода с модулем typing в Python - comments

En
Code typing python (python)