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('Не все строки')
Используется для безопасной работы со смешанными данными.