Python аннотации типов: как работает статическая проверка

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

Статическая типизация в Python: аннотации и проверка

Основное решение для статической типизации в Python состоит в использовании аннотаций типов (type hints) совместно с инструментами статического анализа, такими как mypy или Pyright. Эти инструменты проверяют согласованность типов на этапе разработки, не изменяя поведение программы во время выполнения. Пример:


def greet(name: str) -> str:
    return f'Привет, {name}!'

result: int = greet('Вася')  # ошибка типа

что означает динамическая типизация в python (динамическая типизация в python)

$ mypy test.py
test.py:3: error: Incompatible types in assignment (expression has type 'str', variable has type 'int')

Python статическая типизация (статическая типизация в python)

Аннотация name: str указывает ожидаемый тип аргумента, а -> str тип возвращаемого значения. Переменная result объявлена как int, но функция возвращает строку. mypy выявляет это несоответствие. Цель: повышение читаемости, обнаружение ошибок на раннем этапе. Использование рекомендуется в крупных проектах и при командной разработке.

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

Для этого используются аннотации с типами list[float] (Python 3.9+) или typing.List[float] (для старых версий). Пример:


from typing import List

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

# Python 3.9+
def average_new(numbers: list[float]) -> float:
    return sum(numbers) / len(numbers)

Оба варианта корректно обрабатываются mypy. Важно указывать тип элементов, иначе mypy примет list[Any], что снижает эффективность.

Проблема: импорт устаревшего типа List из typing. Решение: использовать встроенный list с квадратными скобками в Python 3.9 и выше. Другая проблема: аннотация не проверяется во время выполнения, поэтому передача не списка, а другого итерируемого объекта может остаться незамеченной. Для точности используйте Iterable[float].

Как описать функцию, которая может возвращать None?

Применяется Optional[Тип] или Union[Тип, None]. Пример:


from typing import Optional

def find_user(id: int) -> Optional[str]:
    if id > 0:
        return f'User{id}'
    return None

print(find_user(1))  # mypy проверяет, что возвращаемое значение может быть None

Ошибка: забыть импортировать Optional или использовать int | None (Python 3.10+). Решение: применять современный синтаксис объединения.

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

Используется TypeVar и Generic. Пример:


from typing import TypeVar

T = TypeVar('T')

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

# mypy выведет тип, соответствующий переданному списку
print(first_element([1, 2, 3]))  # int
print(first_element(['a', 'b']))  # str

Проблема: неправильное имя TypeVar (например, 't' вместо 'T'). Решение: следовать соглашению об именовании. Также при использовании TypeVar необходимо указывать bound, если тип ограничен.

Как использовать структурную типизацию (утиную типизацию)?

Протоколы из модуля typing позволяют определить структуру класса без наследования. Пример:


from typing import Protocol

class Flyable(Protocol):
    def fly(self) -> str: ...

class Bird:
    def fly(self) -> str:
        return 'Птица летит'

class Airplane:
    def fly(self) -> str:
        return 'Самолет летит'

def make_fly(obj: Flyable) -> str:
    return obj.fly()

print(make_fly(Bird()))  # OK
print(make_fly(Airplane()))  # OK

Проблема: протоколы не проверяются во время выполнения, только статически. Для runtime проверки нужен isinstance. Также класс должен явно реализовывать метод с совпадающей сигнатурой.

Как валидировать данные во время выполнения на основе аннотаций?

Библиотека pydantic использует аннотации типов для автоматической валидации и сериализации. Это не статическая типизация в чистом виде, но тесно связана. Пример:


from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

user = User(name='Анна', age=25)  # OK
# user = User(name='Анна', age='два')  # ValidationError

Проблема: pydantic выполняет проверку только при создании экземпляра, а не на этапе статического анализа. Для статической проверки сочетают pydantic с mypy.

Расширенные примеры статической типизации

В этом разделе приведены менее распространённые и более сложные случаи применения type hints с mypy.

Пример 1: Literal для ограничения значений

Тип Literal позволяет указать конкретные разрешённые значения. Полезен для конфигураций.

Пример

from typing import Literal

def set_mode(mode: Literal['admin', 'user']) -> None:
    print(f'Режим: {mode}')

set_mode('admin')  # OK
set_mode('moderator')  # mypy: error: Argument 1 to 'set_mode' has incompatible type 'Literal['moderator']'; expected 'Literal['admin', 'user']'
$ mypy literal_example.py
literal_example.py:5: error:...

Цель: строгий контроль входных данных на этапе статического анализа.

Пример 2: TypedDict для словарей

TypedDict задаёт структуру словаря с типами ключей.

Пример

from typing import TypedDict

class Person(TypedDict):
    name: str
    age: int

def greet_person(p: Person) -> str:
    return f'Привет, {p["name"]}!'

p: Person = {'name': 'Иван', 'age': 30}
print(greet_person(p))
p_bad: Person = {'name': 'Иван'}  # mypy: missing key 'age'
$ mypy typeddict_example.py
typeddict_example.py:10: error: Key 'age' is missing in TypedDict 'Person'

Используется для строгой типизации конфигураций и ответов API.

Пример 3: Callable с сигнатурой

Аннотация Callable описывает функцию как параметр.

Пример

from typing import Callable

def execute(func: Callable[[int, int], int], x: int, y: int) -> int:
    return func(x, y)

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

print(execute(add, 3, 4))  # 7

Проблема: Callable не проверяет имена параметров, только типы и количество.

Пример 4: Overload для разных сигнатур

Если функция ведёт себя по-разному в зависимости от типа аргумента, используйте overload.

Пример

from typing import overload

@overload
def process(value: int) -> str: ...
@overload
def process(value: str) -> int: ...

def process(value):
    if isinstance(value, int):
        return str(value)
    elif isinstance(value, str):
        return len(value)
    else:
        raise TypeError

print(process(42))   # '42'
print(process('hi')) # 2

mypy проверит соответствие аннотаций реализации.

Пример 5: TypeGuard для сужения типов

TypeGuard позволяет определить пользовательскую проверку типа.

Пример

from typing import TypeGuard

def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in val)

data: list[object] = ['a', 'b', 3]
if is_str_list(data):
    print(data[0].upper())  # mypy знает, что data - list[str]
else:
    print('Не все строки')

Используется для безопасной работы со смешанными данными.

статическая типизация в Python - comments

En
Python статическая типизация (python)