Представьте, что вы открыли чужой файл и видите функцию process(data, limit). Что такое data? Список чисел? Строка? Словарь? А limit — это сколько, максимум элементов или максимум секунд? Без подсказок приходится читать всё тело функции, чтобы догадаться. И хуже: через пару месяцев вы точно так же не вспомните, что имели в виду в своём собственном коде.
Аннотации типов решают эту проблему: вы прямо рядом с переменной или параметром пишете, какой тип ожидается. Код начинает объяснять сам себя, а редактор — подсказывать методы и находить нестыковки прямо во время набора текста.
Аннотации переменных
Простыми словами: аннотация — это записка самому себе (и всем, кто будет читать код после вас) о том, значение какого типа лежит в переменной. Она не меняет поведение программы, а только добавляет подпись.
Официально это называется type hint (подсказка типа) или аннотация типа (type annotation). Синтаксис: после имени переменной ставится двоеточие, затем тип, и уже потом, через =, значение.
age: int = 25
name: str = "Алиса"
price: float = 9.99
is_active: bool = True
count: int
count = 0
Разбор по строкам:
age: int = 25— после имениageдвоеточие и типint, дальше как обычно= 25. Переменная работает точно так же, как без аннотации.- Строки 2–4 — та же схема для строки, дробного числа и логического значения: тип пишется один раз, сразу после имени.
count: intбез значения — так можно объявить, что переменнаяcountбудет хранитьint, но присвоить значение позже. До присваивания использоватьcountнельзя, интерпретатор выдаст обычную ошибку про необъявленное имя.count = 0на следующей строке — фактическое присваивание, здесь тип уже не повторяем: Python помнит, чтоcountаннотирован какint.
Важно понимать: Python — язык с динамической типизацией, и аннотации он не проверяет во время выполнения. Можно написать age: int = "двадцать пять", и программа выполнится без единой ошибки. Аннотации нужны людям и специальным инструментам, а не самому интерпретатору — он их просто запоминает и идёт дальше.
Аннотации параметров и возвращаемого значения
Для функций эта же идея работает ещё сильнее: если у функции много параметров, аннотации избавляют от постоянного заглядывания в тело функции, чтобы понять, что ей передавать и что она вернёт.
Официальный термин здесь тот же — аннотация, только применяется к каждому параметру и к результату. Тип параметра пишется так же, через двоеточие после имени. Тип возвращаемого значения обозначается стрелкой -> перед двоеточием, которое закрывает заголовок функции.
def greet(name: str) -> str:
return f"Привет, {name}!"
def add(a: int, b: int) -> int:
return a + b
def send_message(text: str, repeat: int = 1) -> None:
for _ in range(repeat):
print(text)
Разбор по частям:
def greet(name: str) -> str:— параметрnameдолжен быть строкой, а функция вернёт строку. Послеstrидёт стрелка, а за ней ещё один тип — тип того, что стоит послеreturn.def add(a: int, b: int) -> int:— у каждого параметра своя аннотация, они пишутся отдельно через запятую, как обычные параметры.def send_message(text: str, repeat: int = 1) -> None:— если у параметра есть значение по умолчанию, тип ставится ДО знака равенства:repeat: int = 1, а не наоборот.-> None— так помечают функции, которые ничего не возвращают черезreturnс значением, а просто что-то делают (здесь — печатают).Noneздесь означает «функция явно ничего не возвращает».
Привычка на будущее: даже в маленьком учебном скрипте стоит аннотировать функции. Это не занимает много времени, зато сразу видно контракт функции: что подать на вход и что получить на выходе.
Проверка типа во время выполнения: type() и isinstance()
Аннотации описывают тип «на бумаге», но иногда нужно узнать тип по-настоящему, пока программа работает: например, перед тем как делить, убедиться, что в переменной действительно число, а не строка.
Для этого есть две встроенные функции. type(объект) возвращает тип объекта. А isinstance(объект, тип) возвращает True или False — является ли объект экземпляром указанного типа.
x = 42
print(type(x)) # <class 'int'>
print(type(x) == int) # True
print(isinstance(x, int)) # True
print(isinstance(x, str)) # False
# Проверка сразу на несколько типов — типы передаются кортежем
print(isinstance(x, (int, float))) # True
Разбор:
type(x)— вернёт сам тип, в примере это классint. При печати он выглядит как<class 'int'>.type(x) == int— сравнение типа с конкретным типом через==, дастTrue, потому чтоxдействительноint.isinstance(x, int)— тот же вопрос, но заданный по-другому: «является лиxэкземпляромint?». Первый аргумент — сам объект, второй — тип, который проверяем.isinstance(x, (int, float))— когда вторым аргументом передан кортеж типов в круглых скобках, функция вернётTrue, если объект относится хотя бы к одному из них. Так удобно проверять «любое число».
isinstance() в реальном коде используют чаще, чем type() ==: она умеет работать не только с простыми типами, но и корректно ведёт себя, когда типы связаны наследованием (это пригодится в курсе про ООП). Для наших сегодняшних задач достаточно запомнить: isinstance() — основной инструмент проверки типа в рабочем коде, type() — удобна для быстрой отладки и печати.
Модуль typing: аннотации для составных типов
int и str аннотировать просто. А как подписать список строк? Или словарь, где ключ — строка, а значение — число? Для таких случаев в стандартной библиотеке есть модуль typing, который мы подключаем через уже знакомый import.
from typing import List, Dict, Tuple, Optional, Union, Any
Здесь from typing import ... — та же конструкция, что вы уже использовали для from statistics import mean: подключаем не весь модуль, а сразу несколько конкретных имён из него, чтобы дальше писать их без typing. впереди.
List — список с известным типом элементов
def get_names() -> List[str]:
return ["Аня", "Боб", "Влад"]
def total(nums: List[int]) -> int:
return sum(nums)
List[str] читается как «список, а внутри квадратных скобок — тип элементов»: список строк. List[int] — список целых чисел. Квадратные скобки здесь играют ту же роль, что и при обращении по индексу, но в аннотации они уточняют тип содержимого, а не достают элемент.
Dict — словарь с известными типами ключей и значений
def word_lengths(words: List[str]) -> Dict[str, int]:
result: Dict[str, int] = {}
for word in words:
result[word] = len(word)
return result
Dict[str, int] — словарь, где первый тип в скобках относится к ключам, второй — к значениям: здесь ключ — слово (строка), значение — его длина (целое число). Обратите внимание: строка result: Dict[str, int] = {} аннотирует и параметр функции, и локальную переменную одновременно — это те же аннотации переменных из первого раздела, просто со сложным типом.
Tuple — кортеж с фиксированным набором типов по порядку
def get_person() -> Tuple[str, int, bool]:
return "Мария", 30, True
Tuple[str, int, bool] расписывает тип каждого элемента кортежа по позиции: первый элемент — строка (имя), второй — целое число (возраст), третий — логическое значение. В отличие от List, где в скобках всегда один тип для всех элементов, у Tuple типов может быть несколько — ровно столько, сколько элементов в кортеже.
Optional — значение либо None
def find_user(user_id: int, users: Dict[int, str]) -> Optional[str]:
return users.get(user_id)
users.get(user_id) возвращает значение по ключу либо None, если ключа нет — вы уже пользовались этим методом словарей. Optional[str] честно сообщает: «эта функция вернёт либо строку, либо None». Писать просто str здесь было бы неточно: тогда пришлось бы врать, что None никогда не случится.
Union — один из нескольких заранее известных типов
def stringify(value: Union[int, float, str]) -> str:
return str(value)
Union[int, float, str] означает «int, или float, или str — любой из перечисленных». Внутри функции всё равно можно позвать str(value), потому что все три типа умеют превращаться в строку. Кстати, теперь видно, что Optional[str] из прошлого блока — это просто короткая запись для Union[str, None].
Any — тип действительно любой
def log(value: Any) -> None:
print(f"[LOG] {value}")
Any буквально означает «здесь может быть что угодно, не будем это уточнять». Используйте его только когда тип на самом деле не важен для работы функции — как в этом логгере, который просто печатает значение любой природы. Если ставить Any везде, где лень подумать над настоящим типом, аннотации теряют весь смысл.
Короткий синтаксис Python 3.10+: X | Y
Писать Union[int, str] каждый раз чуть громоздко. Начиная с Python 3.10 разработчики Python добавили более короткую запись через вертикальную черту.
Официально черта | здесь называется оператором объединения типов. Смысл ровно тот же, что у Union, просто компактнее и без импорта из typing.
def old_style(x: Union[int, str]) -> Optional[str]:
return str(x) if x else None
def new_style(x: int | str) -> str | None:
return str(x) if x else None
def process(value: int | float | str | None) -> str:
return str(value) if value is not None else "пусто"
def get_ids() -> list[int]:
return [1, 2, 3]
Разбор:
old_styleиnew_style— одна и та же аннотация, записанная двумя способами:Union[int, str]иint | strозначают одно и то же, точно так жеOptional[str]иstr | None.int | float | str | None— через|можно перечислить сколько угодно типов подряд, включаяNone, без отдельногоOptional.list[int]— с версии Python 3.9 встроенные коллекции (list,dict,tuple,set) можно указывать с типом элементов напрямую, маленькими буквами, без импортаListизtyping. Смысл тот же, что уList[int]из прошлого раздела.
Совместимость: запись X | Y и строчные list[int]/dict[str, int] работают только в достаточно новых версиях Python. Если код должен запускаться и на старых версиях — используйте Union, Optional и List/Dict из typing. В новых проектах короткий синтаксис уже стал стандартом.
Статическая проверка типов: mypy
Мы выяснили, что сам Python аннотации в момент запуска не проверяет. Но кроме интерпретатора есть отдельные программы, которые умеют читать код не запуская его и искать несовпадения типов заранее — ещё до того, как вы нажали «запустить».
Такой подход называется статической проверкой типов, а самый известный инструмент для Python — mypy. Слово «статическая» здесь означает «без выполнения программы», в отличие от «динамической» проверки, которая происходит уже во время работы кода.
Установка (в терминале, один раз):
pip install mypy
Файл с ошибкой типа, check.py:
def add(a: int, b: int) -> int:
return a + b
result = add("привет", 5)
Запуск проверки и её ответ:
mypy check.py
check.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)
mypy прочитал аннотацию a: int, увидел, что в четвёртой строке вместо числа передана строка "привет", и сообщил об этом — программа при этом ни разу не запускалась. В больших проектах, где функции вызывают друг друга сотни раз, это ловит целый класс ошибок ещё на этапе написания кода, а не после жалобы пользователей.
mypy — не единственный такой инструмент, есть и другие (например, pyright, встроенный в некоторые редакторы). Но именно mypy чаще всего рекомендуют для первого знакомства со статической проверкой типов.
Частые ошибки
Ждать, что аннотация сама преобразует или проверит значение. Это просто подпись, а не проверка:
age: int = "25"
print(age + 1)
# TypeError: can only concatenate str (not "int") to str
# Аннотация не превратила строку в число — ошибка вылезла позже, в другом месте
Забыть импорт из typing перед использованием List, Dict, Optional и других составных типов.
def get_ids() -> List[int]:
return [1, 2, 3]
# NameError: name 'List' is not defined
# Нужно сначала: from typing import List
Перепутать порядок в аннотации параметра со значением по умолчанию. Тип — всегда до знака равенства:
def repeat(text: str = "hi": int):
pass
# SyntaxError — правильно: def repeat(text: str, times: int = 1):
Что важно запомнить
- Аннотации — подсказки для людей и инструментов, Python их не проверяет во время выполнения
- Переменная:
x: int = 5; функция:def f(a: int) -> str:; тип параметра со значением по умолчанию — до знака= type(x)— узнать тип объекта;isinstance(x, тип)— проверить принадлежность к типу, основной способ в рабочем коде- Модуль
typing:List[T],Dict[K, V],Tuple[...],Optional[T](то же, чтоUnion[T, None]),Union[...],Any - С Python 3.10 короче:
Union[int, str]→int | str,Optional[str]→str | None; с 3.9 —list[int]вместоList[int] mypy— статический анализатор: читает аннотации и находит несовпадения типов до запуска программы
Проверьте себя
7 вопросов
Аннотации типов в функциях
Добавьте аннотации типов к следующим функциям, не меняя их код:
greet(name)— принимает строку, возвращает строкуadd(a, b)— принимает два целых числа, возвращает целое числоis_even(n)— принимает целое число, возвращает boolget_squares(nums)— принимает список целых чисел, возвращает список целых чисел