Аннотации типов Python: полное практическое пособие
Основные принципы аннотации типов
Современный Python (начиная с версии 3.10) позволяет использовать оператор | для объединения типов, что заменяет устаревшую конструкцию Union. Это делает код более лаконичным и интуитивно понятным.
def greet(name: str | None) -> str:
if name is None:
return "Hello, stranger!"
return f"Hello, {name}!"Python типизация переменных (типизация переменных в python)
Также активно используется list, dict и другие встроенные дженерики без импорта из typing (доступно с Python 3.9).
Типичные ошибки:
- Забыть импортировать
OptionalилиUnionв проектах, поддерживающих Python < 3.10. - Использовать
List[int]вместоlist[int]в коде, рассчитанном на Python 3.9+, что усложняет чтение. - Путать
Optional[int]иint | None(семантически одинаково, но вариант с|короче).
Как аннотировать простые переменные и функции?
Для переменных и функций используются базовые типы: int, float, str, bool, bytes и т.д.
x: int = 42
y: str = "hello"
def add(a: int, b: int) -> int:
return a + bаннотация типов python (аннотация типов в python)
Проблема: аннотации не проверяются во время выполнения. Ошибки типов обнаруживаются только статическими анализаторами (mypy, pyright).
Как указать, что функция ничего не возвращает или возвращает разные типы?
Используйте None или Union/| для нескольких типов.
def log(message: str) -> None:
print(message)
def get_status(code: int) -> str | int:
if code == 200:
return "OK"
return code
Ошибка:
Неверное указание возвращаемого типа может привести к ложным срабатываниям анализатора.
Как аннотировать коллекции (списки, словари, кортежи)?
В Python 3.9+ коллекции аннотируются напрямую: list[int], dict[str, float]. Для версий ниже используйте typing.List, typing.Dict.
from typing import List, Dict
# современный способ (Python 3.9+)
scores: list[int] = [90, 85, 88]
prices: dict[str, float] = {"apple": 1.5, "banana": 0.8}
# устаревший (Python < 3.9)
scores_old: List[int] = [90, 85, 88]
Проблема: для кортежей фиксированной длины используйте tuple[int, str, float], для переменной длины - tuple[int, ...].
Как обрабатывать необязательные параметры и None?
Используйте Optional (до Python 3.10) или | None.
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
# возвращает имя или None
return "Alice" if user_id == 1 else None
# эквивалент с |
def find_user_new(user_id: int) -> str | None:
return "Alice" if user_id == 1 else None
Ошибка:
Не путать Optional[int] (эквивалент Union[int, None]) с необязательным аргументом def func(x: int = 5) - это разные концепции.
Как аннотировать функции с *args и **kwargs?
Для *args используется кортеж элементов, для **kwargs - словарь.
def sum_all(*args: int) -> int:
return sum(args)
def print_kwargs(**kwargs: str) -> None:
for key, value in kwargs.items():
print(f"{key}: {value}")
Проблема: для kwargs часто указывают общий тип значения (str), но это не даёт контроля над отдельными ключами.
Как использовать дженерики (TypeVar) для обобщённого кода?
TypeVar позволяет создавать функции, работающие с любым типом, сохраняя типизацию.
from typing import TypeVar
T = TypeVar('T')
def first_element(items: list[T]) -> T:
return items[0]
# пример
result = first_element([1, 2, 3]) # result: int
result2 = first_element(["a", "b"]) # result: str
Ошибка: забыть привязать TypeVar к конкретному классу (T = TypeVar('T', bound=int)) может привести к нежелательным операциям.
Как аннотировать callable (функции высшего порядка)?
Используйте Callable[[arg_types], return_type] из typing.
from typing import Callable
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
result = apply(lambda x, y: x + y, 3, 4) # 7
Проблема: для сложных сигнатур с *args используйте Callable[..., ReturnType].
Как аннотировать self и возвращаемый тип в классах?
Для self обычно не указывают тип, но при необходимости можно использовать Self (Python 3.11+).
from typing import Self
class MyClass:
def set_value(self, value: int) -> Self:
self.value = value
return self
Ошибка: в версиях до 3.11 для возврата экземпляра класса используют "MyClass" (строковая аннотация) или TypeVar.
Как использовать Protocol для структурной типизации (утиная типизация)?
Protocol позволяет определить минимальный набор методов/атрибутов, которым должен обладать объект.
from typing import Protocol
class Flyable(Protocol):
def fly(self) -> None: ...
def make_fly(obj: Flyable) -> None:
obj.fly()
class Bird:
def fly(self) -> None:
print("Bird flying")
make_fly(Bird()) # корректно
Проблема: Protocol не проверяется во время выполнения; статический анализатор может пропустить ошибку, если метод не реализован.
Расширенные примеры аннотаций типов
1. Аннотации с использованием Literal (Python 3.8+)
Тип Literal позволяет ограничить значение конкретными литералами (строками, числами, булевыми).
from typing import Literal
def set_mode(mode: Literal["read", "write", "append"]) -> str:
if mode == "read":
return "Opening file for reading"
elif mode == "write":
return "Opening file for writing"
else:
return "Opening file for appending"
print(set_mode("read"))
Opening file for reading
Если передать недопустимое значение, статический анализатор выдаст ошибку.
2. TypedDict для строгой типизации словарей
TypedDict определяет структуру словаря с конкретными ключами и типами значений.
from typing import TypedDict
class Person(TypedDict):
name: str
age: int
email: str | None
def process_person(p: Person) -> str:
return f"{p['name']} (age {p['age']})"
data: Person = {"name": "Alice", "age": 30, "email": None}
print(process_person(data))
Alice (age 30)
Примечание:
TypedDict не проверяется во время выполнения, только статически. Для проверки во время выполнения можно использовать dataclass.
3. ParamSpec и Concatenate для декораторов, сохраняющих сигнатуру (Python 3.10+)
ParamSpec позволяет декоратору передавать все параметры оборачиваемой функции, сохраняя их типы.
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec('P')
T = TypeVar('T')
def log_call(func: Callable[P, T]) -> Callable[P, T]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def add(a: int, b: int) -> int:
return a + b
result = add(2, 3)
print(result)
Calling add 5
4. Аннотации с использованием Self (Python 3.11+) для возврата текущего класса
Self упрощает паттерн builder и цепочки методов.
from typing import Self
class Builder:
def __init__(self):
self._items: list[str] = []
def add(self, item: str) -> Self:
self._items.append(item)
return self
def build(self) -> str:
return ', '.join(self._items)
b = Builder().add("apple").add("banana")
print(b.build())
apple, banana
5. Аннотации для асинхронных функций (async/await)
Асинхронные функции аннотируются как обычные, но возвращаемый тип обычно Coroutine или конкретный тип с обёрткой.
import asyncio
from typing import Coroutine
async def fetch_data(url: str) -> dict:
# имитация запроса
await asyncio.sleep(1)
return {"status": 200, "data": "ok"}
async def main() -> None:
result = await fetch_data("http://example.com")
print(result)
asyncio.run(main())
{'status': 200, 'data': 'ok'}
Если функция возвращает Coroutine, можно указать типы передаваемых и возвращаемых значений: Coroutine[Any, Any, dict].
6. Аннотации с использованием TypeGuard (Python 3.10+)
TypeGuard позволяет пользовательской функции проверки типа сужать тип переменной для анализатора.
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
def process(items: list[object]) -> None:
if is_string_list(items):
# теперь items считается list[str]
print(' '.join(items))
else:
print("Not all strings")
process(["hello", "world"])
hello world
7. Аннотации для исключений (raise/except) с помощью Never (Python 3.11+)
Тип Never указывает, что функция никогда не завершается нормально (например, всегда выбрасывает исключение).
from typing import Never
def abort(message: str) -> Never:
raise SystemExit(message)
def risky() -> int:
abort("Fatal error")
return 0 # этот код недостижим
8. Аннотации с использованием overload (перегрузка функций)
Декоратор @overload позволяет описать несколько сигнатур одной функции для разных типов аргументов.
from typing import overload
@overload
def process(data: int) -> str: ...
@overload
def process(data: str) -> int: ...
def process(data: int | str) -> str | int:
if isinstance(data, int):
return str(data)
else:
return len(data)
print(process(42)) # str
print(process("hi")) # int
42 2
Перегрузки улучшают автодополнение и проверку типов.