Аннотации типов (type hints) в классах и методах
Вы уже умеете документировать код через docstring. Теперь узнаем ещё один мощный инструмент — аннотации типов. Они позволяют прямо в коде указывать, какого типа должны быть параметры и что возвращает функция. IDE и инструменты используют это для поиска ошибок ещё до запуска программы.
🤔 Что такое аннотации типов?
Представьте, что вы видите такую функцию впервые:
def process(data, limit):
...
Что такое data? Строка? Список? Словарь? Что такое limit? Число? Булево значение? Непонятно. Приходится читать весь код функции, чтобы догадаться.
Теперь сравните с этой версией:
def process(data: list[str], limit: int) -> bool:
...
Сразу ясно: data — список строк, limit — целое число, функция возвращает булево значение. Это и есть аннотации типов (их также называют type hints — «подсказки типов»).
⚡ Самое важное — запомните это навсегда!
Аннотации типов в Python — это только подсказки. Python НЕ проверяет их во время выполнения программы (runtime). Вы можете написать x: int = "hello" — и Python не выдаст ошибку. Аннотации существуют для людей (читаемость кода) и для инструментов (IDE, анализаторы кода).
Аннотации типов появились в Python 3.5 (PEP 484) и с каждой версией становятся удобнее. Сегодня они — стандарт в профессиональном Python-коде.
📝 Базовый синтаксис аннотаций
Синтаксис простой. Разберём его по частям:
📥
Параметр функции
name: тип
После имени параметра ставится двоеточие, затем тип.
📤
Возвращаемое значение
-> тип:
После скобок с параметрами, перед двоеточием.
🏷️
Атрибут класса
name: тип
В теле класса или в __init__.
Смотрим на живой пример с базовыми типами:
# Базовые типы Python
# int — целые числа: 1, -5, 100
# float — числа с плавающей точкой: 3.14, -0.5
# str — строки: "hello", 'world'
# bool — булево значение: True, False
# None — отсутствие значения (функция ничего не возвращает)
def greet(name: str) -> str:
"""Возвращает приветствие для пользователя."""
return f"Привет, {name}!"
def add(a: int, b: int) -> int:
"""Складывает два целых числа."""
return a + b
def calculate_area(radius: float) -> float:
"""Вычисляет площадь круга по радиусу."""
return 3.14159 * radius ** 2
def is_adult(age: int) -> bool:
"""Проверяет, является ли человек совершеннолетним."""
return age >= 18
def print_message(message: str) -> None:
"""Выводит сообщение. Ничего не возвращает."""
print(message)
# Использование — всё как обычно
print(greet("Алиса")) # Привет, Алиса!
print(add(3, 5)) # 8
print(is_adult(20)) # True
💡 Про -> None: когда функция ничего не возвращает (только выполняет действие), принято явно указывать -> None. Это сигнализирует читателю: «эта функция что-то делает, но не возвращает результат».
🔬 Демонстрация: Python игнорирует аннотации в runtime
Это очень важно понять. Запустим код, который «нарушает» аннотации:
def add(a: int, b: int) -> int:
return a + b
# «Нарушаем» аннотации — передаём строки вместо чисел
result = add("hello", " world")
print(result) # hello world — работает! Нет ошибки!
print(type(result)) # <class 'str'>
# Ещё пример
def greet(name: str) -> str:
return f"Привет, {name}!"
greet(12345) # Тоже работает! Python не ругается.
greet([1, 2]) # И это работает!
# Аннотации можно посмотреть через __annotations__
print(add.__annotations__)
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
# Они просто хранятся как словарь — Python их не использует сам
🤷 Зачем тогда они нужны, если Python их не проверяет?
- IDE (PyCharm, VS Code) — подчёркивает ошибки прямо в редакторе и подсказывает автодополнение
- mypy — отдельный инструмент, который проверяет типы до запуска программы (статический анализ)
- Документация — коллеги сразу видят, что ожидает функция, без чтения её тела
- Рефакторинг — при переименовании или изменении типа IDE находит все места, где нужно обновить код
📦 Модуль typing: Optional, Union и другие
Базовых типов (int, str, float) часто недостаточно. Что если параметр может быть числом или строкой? Или может быть None? Для таких случаев в Python есть модуль typing.
Optional[X]
«Либо X, либо None». Самый частый случай — параметр со значением по умолчанию None.
Union[X, Y]
«Либо X, либо Y». Когда параметр может быть одним из нескольких типов.
List[X]
Список элементов типа X. Например, List[int] — список целых чисел.
Dict[K, V]
Словарь с ключами типа K и значениями типа V.
Tuple[X, Y]
Кортеж с фиксированной структурой. Tuple[int, str] — пара (число, строка).
Any
Любой тип. Анализаторы не проверяют этот параметр. Используйте как последнее средство.
from typing import Optional, Union, List, Dict, Tuple
# Optional[str] означает: строка ИЛИ None
# Это то же самое, что Union[str, None]
def find_user(user_id: int) -> Optional[str]:
"""Ищет пользователя по ID. Возвращает имя или None если не найден."""
users = {1: "Алиса", 2: "Боб"}
return users.get(user_id) # get() возвращает None если ключ не найден
print(find_user(1)) # Алиса
print(find_user(99)) # None
# Union — параметр может быть int или float
def double(value: Union[int, float]) -> Union[int, float]:
"""Удваивает число (работает и с int, и с float)."""
return value * 2
print(double(5)) # 10
print(double(2.5)) # 5.0
# List[str] — список строк
def format_names(names: List[str]) -> str:
"""Объединяет список имён через запятую."""
return ", ".join(names)
print(format_names(["Алиса", "Боб", "Карл"])) # Алиса, Боб, Карл
# Dict[str, int] — словарь где ключи — строки, значения — числа
def total_score(scores: Dict[str, int]) -> int:
"""Суммирует все очки в словаре."""
return sum(scores.values())
print(total_score({"Алиса": 10, "Боб": 15})) # 25
# Tuple[str, int] — кортеж (имя, возраст)
def describe_person(person: Tuple[str, int]) -> str:
name, age = person
return f"{name}, {age} лет"
print(describe_person(("Алиса", 30))) # Алиса, 30 лет
🏗️ Аннотации типов в классах
В классах аннотации используются в двух местах: для атрибутов (данных класса) и для методов (функций класса). Всё работает точно так же, как и для обычных функций.
from typing import Optional, List
class Student:
# Аннотации атрибутов класса — на уровне класса (без значений)
# Это «объявление» атрибутов — IDE знает, что они будут
name: str
age: int
grade: float
email: Optional[str] # email может быть не указан
def __init__(self, name: str, age: int, grade: float,
email: Optional[str] = None) -> None:
# Аннотации атрибутов через self — IDE подхватывает тип
self.name = name
self.age = age
self.grade = grade
self.email = email
def introduce(self) -> str:
"""Возвращает строку-представление студента."""
return f"Студент {self.name}, {self.age} лет, оценка: {self.grade}"
def is_excellent(self) -> bool:
"""Проверяет, является ли студент отличником (оценка >= 4.5)."""
return self.grade >= 4.5
def update_email(self, new_email: str) -> None:
"""Обновляет адрес электронной почты студента."""
self.email = new_email
@staticmethod
def validate_grade(grade: float) -> bool:
"""Проверяет, что оценка в допустимом диапазоне (0.0 — 5.0)."""
return 0.0 <= grade <= 5.0
class Group:
name: str
students: List[Student] # список объектов типа Student
def __init__(self, name: str) -> None:
self.name = name
self.students = []
def add_student(self, student: Student) -> None:
"""Добавляет студента в группу."""
self.students.append(student)
def get_average_grade(self) -> float:
"""Возвращает среднюю оценку по группе."""
if not self.students:
return 0.0
total = sum(s.grade for s in self.students)
return total / len(self.students)
def find_by_name(self, name: str) -> Optional[Student]:
"""Ищет студента по имени. Возвращает None если не найден."""
for student in self.students:
if student.name == name:
return student
return None
# Использование
alice = Student("Алиса", 20, 4.8, "alice@example.com")
bob = Student("Боб", 22, 3.7) # email не указан — None по умолчанию
print(alice.introduce()) # Студент Алиса, 20 лет, оценка: 4.8
print(alice.is_excellent()) # True
print(bob.is_excellent()) # False
group = Group("Группа 101")
group.add_student(alice)
group.add_student(bob)
print(group.get_average_grade()) # 4.25
print(group.find_by_name("Алиса")) # объект Student
print(group.find_by_name("Карл")) # None
🎯 Совет: аннотируйте метод __init__ с возвращаемым типом -> None, так как конструктор технически ничего не возвращает. Аннотируйте self не нужно — Python и инструменты знают его тип автоматически.
🚀 Современный синтаксис: Python 3.9+ и 3.10+
До Python 3.9 для аннотирования контейнеров (списков, словарей и т.д.) нужно было импортировать специальные типы из модуля typing. С Python 3.9 это больше не нужно — можно использовать встроенные типы напрямую:
# Старый стиль (Python 3.5 — 3.8) — нужен импорт
from typing import List, Dict, Tuple, Set, Optional, Union
def old_style(names: List[str], scores: Dict[str, int]) -> Tuple[str, int]:
...
# Новый стиль (Python 3.9+) — импорт не нужен!
def new_style(names: list[str], scores: dict[str, int]) -> tuple[str, int]:
...
# Другие встроенные типы тоже работают:
def process(data: set[int], items: frozenset[str]) -> list[bool]:
...
Python 3.10 принёс ещё одно улучшение — оператор | вместо Union и Optional:
# Старый стиль (Python 3.5 — 3.9)
from typing import Optional, Union
def find_user(user_id: int) -> Optional[str]: # str или None
...
def parse(value: Union[int, float, str]) -> str: # int или float или str
...
# Новый стиль (Python 3.10+) — оператор | вместо Optional и Union
def find_user(user_id: int) -> str | None: # str или None — без Optional!
...
def parse(value: int | float | str) -> str: # через | — без Union!
...
# Пример класса с современным синтаксисом
class Library:
books: list[str]
catalog: dict[str, int] # название -> количество экземпляров
def __init__(self) -> None:
self.books = []
self.catalog = {}
def search(self, query: str) -> list[str] | None:
"""Ищет книги по запросу. Возвращает список или None."""
results = [b for b in self.books if query.lower() in b.lower()]
return results if results else None
📊 Таблица: старый vs новый синтаксис
| Смысл | Python 3.5–3.8 | Python 3.9+ | Python 3.10+ |
|---|---|---|---|
| Список строк | List[str] |
list[str] |
list[str] |
| Словарь str→int | Dict[str, int] |
dict[str, int] |
dict[str, int] |
| str или None | Optional[str] |
Optional[str] |
str | None |
| int или str | Union[int, str] |
Union[int, str] |
int | str |
🔄 Forward references — когда класс ещё не определён
Иногда в аннотации нужно использовать класс, который ещё не был определён в коде. Например, метод класса Node возвращает объект Node, но класс ещё не дочитан Python до конца. Это вызовет ошибку NameError:
# ОШИБКА! Python читает код сверху вниз.
# Когда он доходит до аннотации -> Node,
# класс Node ещё не определён полностью.
class Node:
def __init__(self, value: int) -> None:
self.value = value
self.next: Node = None # NameError: name 'Node' is not defined
Решение — взять имя класса в кавычки. Тогда Python не пытается найти имя прямо сейчас — он запомнит строку и инструменты разберут её позже:
# РЕШЕНИЕ 1: Имя класса в кавычках (forward reference)
class Node:
def __init__(self, value: int) -> None:
self.value = value
self.next: "Node | None" = None # Строка — Python не проверяет сразу
def set_next(self, node: "Node") -> None:
"""Устанавливает следующий узел в цепочке."""
self.next = node
# РЕШЕНИЕ 2: from __future__ import annotations (Python 3.7+)
# Делает ВСЕ аннотации «ленивыми» — они не вычисляются сразу
from __future__ import annotations
class TreeNode:
def __init__(self, value: int) -> None:
self.value = value
self.left: TreeNode | None = None # Теперь работает без кавычек!
self.right: TreeNode | None = None # Потому что импорт выше
def insert(self, child: TreeNode) -> None:
"""Добавляет дочерний узел."""
if self.left is None:
self.left = child
else:
self.right = child
# Использование
root = TreeNode(10)
left = TreeNode(5)
right = TreeNode(15)
root.insert(left)
root.insert(right)
print(root.left.value) # 5
print(root.right.value) # 15
💡 Когда возникают forward references?
- Метод класса принимает или возвращает объект того же класса (рекурсивные структуры: дерево, связный список)
- Два класса ссылаются друг на друга — тогда один из них ещё не определён при чтении аннотации другого
🔍 mypy — статический анализатор типов
Мы уже знаем, что Python сам не проверяет типы. Но есть специальный инструмент — mypy, — который делает именно это. Он читает ваш код, смотрит на аннотации и сообщает об ошибках до запуска программы.
Представьте mypy как «компилятор типов» — он проверяет ваш код так, как это делает компилятор в C++ или Java, но без реального запуска.
⚙️ Установка и запуск mypy
# Установка через pip
pip install mypy
# Проверка одного файла
mypy my_file.py
# Проверка всего проекта
mypy .
# Строгий режим — проверяет больше вещей
mypy --strict my_file.py
# Файл: example.py
def add(a: int, b: int) -> int:
return a + b
# Ошибки, которые найдёт mypy:
result = add("hello", "world") # Ошибка! ожидается int, передана str
result = add(1, 2, 3) # Ошибка! слишком много аргументов
name: str = 123 # Ошибка! ожидается str, присвоен int
def greet(name: str) -> int: # Аннотация говорит -> int
return "Привет, " + name # Ошибка! возвращается str, а не int
# Запускаем mypy:
# $ mypy example.py
#
# example.py:5: error: Argument 1 to "add" has incompatible type "str"; expected "int"
# example.py:6: error: Argument 2 to "add" has incompatible type "str"; expected "int"
# example.py:7: error: Too many arguments for "add"
# example.py:9: error: Incompatible types in assignment (expression has type "int", variable has type "str")
# example.py:13: error: Incompatible return value type (got "str", expected "int")
# Found 5 errors in 1 file
✅ Что проверяет mypy?
- Передаются ли аргументы правильных типов в функции
- Совпадает ли тип возвращаемого значения с аннотацией
- Правильно ли используются атрибуты объектов
- Обрабатывается ли
Noneтам, где возвращаетсяOptional
🔧 mypy и None-безопасность
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
users = {1: "Алиса"}
return users.get(user_id)
# mypy найдёт ошибку: find_user может вернуть None,
# но мы используем результат без проверки
name = find_user(1)
print(name.upper()) # Ошибка mypy! name может быть None
# Правильно: сначала проверить на None
name = find_user(1)
if name is not None:
print(name.upper()) # Теперь mypy доволен
🎛️ Callable и Any — аннотации функций и «отключение» проверки
Иногда параметром функции является другая функция. Для таких случаев есть тип Callable. А когда тип совсем непредсказуем — есть Any.
from typing import Callable, Any
# Callable[[типы_аргументов], тип_возврата]
# Callable[[int, int], int] — функция, принимающая два int и возвращающая int
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
"""Применяет функцию func к двум числам."""
return func(a, b)
def multiply(x: int, y: int) -> int:
return x * y
print(apply(multiply, 3, 4)) # 12
print(apply(lambda x, y: x + y, 10, 5)) # 15
# Callable без указания сигнатуры — просто «любая функция»
def run(callback: Callable) -> None:
callback()
# Any — отключает проверку типов для этого параметра
# Используйте как последнее средство!
def process_unknown(data: Any) -> Any:
"""Обрабатывает данные неизвестного типа."""
return data # mypy не будет проверять data
⚠️ Про Any: использование Any «отключает» проверку типов для этого параметра — mypy перестаёт его анализировать. Злоупотребление Any делает аннотации бесполезными. Применяйте только когда действительно не знаете тип (например, при работе с данными от внешнего API).
✅ Итоги урока
- Аннотации типов — это подсказки о типах параметров и возвращаемых значений. Синтаксис:
param: типи-> тип. - Python НЕ проверяет аннотации в runtime — они существуют для людей, IDE и инструментов (mypy).
- Базовые типы:
int,str,float,bool,None— используются напрямую. - Модуль typing даёт
Optional,Union,List,Dict,Tuple,Callable,Any. - Python 3.9+: можно писать
list[str]вместоList[str]— импорт из typing не нужен. - Python 3.10+: оператор
|вместоUnionиOptional:str | NoneвместоOptional[str]. - Forward reference: если класс ещё не определён — берём его имя в кавычки:
"ClassName". - mypy — статический анализатор, который проверяет типы до запуска. Установка:
pip install mypy, запуск:mypy файл.py.
👉 В следующем уроке мы изучим dataclasses — специальный декоратор Python, который автоматически генерирует __init__, __repr__, __eq__ и другие методы на основе аннотаций типов. Аннотации типов там будут использоваться «по-настоящему» — dataclasses используют их для создания кода!