Перейти к содержанию

Введение в аннотации типов Python

Python имеет поддержку необязательных аннотаций типов.

Аннотации типов являются специальным синтаксисом, который позволяет определять тип переменной.

Объявление типов для переменных позволяет улучшить поддержку вашего кода редакторами и различными инструментами.

Это просто краткое руководство / напоминание об аннотациях типов в Python. Оно охватывает только минимум, необходимый для их использования с FastAPI... что на самом деле очень мало.

FastAPI целиком основан на аннотациях типов, у них много выгод и преимуществ.

Но даже если вы никогда не используете FastAPI, вам будет полезно немного узнать о них.

Note

Если вы являетесь экспертом в Python и уже знаете всё об аннотациях типов, переходите к следующему разделу.

Мотивация

Давайте начнем с простого примера:

def get_full_name(first_name, last_name):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

Вызов этой программы выводит:

John Doe

Функция делает следующее:

  • Принимает first_name и last_name.
  • Преобразует первую букву содержимого каждой переменной в верхний регистр с title().
  • Соединяет их через пробел.
def get_full_name(first_name, last_name):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

Отредактируем пример

Это очень простая программа.

А теперь представьте, что вы пишете её с нуля.

В какой-то момент вы бы начали определение функции, у вас были бы готовы параметры...

Но затем вы должны вызвать «тот метод, который преобразует первую букву в верхний регистр».

Было это upper? Или uppercase? first_uppercase? capitalize?

Тогда вы попробуете с давним другом программиста: автодополнением редактора.

Вы вводите первый параметр функции, first_name, затем точку (.), а затем нажимаете Ctrl+Space, чтобы запустить дополнение.

Но, к сожалению, ничего полезного не выходит:

Добавим типы

Давайте изменим одну строчку в предыдущей версии.

Мы изменим именно этот фрагмент, параметры функции, с:

    first_name, last_name

на:

    first_name: str, last_name: str

Вот и все.

Это аннотации типов:

def get_full_name(first_name: str, last_name: str):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

Это не то же самое, что объявление значений по умолчанию, например:

    first_name="john", last_name="doe"

Это другая вещь.

Мы используем двоеточия (:), а не равно (=).

И добавление аннотаций типов обычно не меняет происходящего по сравнению с тем, что произошло бы без неё.

Но теперь представьте, что вы снова находитесь в процессе создания этой функции, но уже с аннотациями типов.

В тот же момент вы пытаетесь запустить автодополнение с помощью Ctrl+Space и вы видите:

При этом вы можете просматривать варианты, пока не найдёте подходящий:

Больше мотивации

Проверьте эту функцию, она уже имеет аннотации типов:

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + age
    return name_with_age

Поскольку редактор знает типы переменных, вы получаете не только дополнение, но и проверки ошибок:

Теперь вы знаете, что вам нужно исправить, преобразовав age в строку с str(age):

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + str(age)
    return name_with_age

Объявление типов

Вы только что видели основное место для объявления подсказок типов. В качестве параметров функции.

Это также основное место, где вы можете использовать их с FastAPI.

Простые типы

Вы можете объявить все стандартные типы Python, а не только str.

Вы можете использовать, к примеру:

  • int
  • float
  • bool
  • bytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
    return item_a, item_b, item_c, item_d, item_d, item_e

Generic-типы с параметрами типов

Существуют некоторые структуры данных, которые могут содержать другие значения, например, dict, list, set и tuple. И внутренние значения тоже могут иметь свой тип.

Чтобы объявить эти типы и внутренние типы, вы можете использовать стандартный Python-модуль typing.

Он существует специально для поддержки подсказок этих типов.

List

Например, давайте определим переменную как list, состоящий из str.

Импортируйте List из typing (с заглавной L):

from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)
🤓 Other versions and variants
def process_items(items: list[str]):
    for item in items:
        print(item)

Объявите переменную с тем же синтаксисом двоеточия (:).

В качестве типа укажите List.

Поскольку список является типом, содержащим некоторые внутренние типы, вы помещаете их в квадратные скобки:

from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)
🤓 Other versions and variants
def process_items(items: list[str]):
    for item in items:
        print(item)

Tip

Эти внутренние типы в квадратных скобках называются «параметрами типов».

В этом случае str является параметром типа, передаваемым в List.

Это означает: "переменная items является list, и каждый из элементов этого списка является str".

Если вы будете так поступать, редактор может оказывать поддержку даже при обработке элементов списка:

Без типов добиться этого практически невозможно.

Обратите внимание, что переменная item является одним из элементов списка items.

И все же редактор знает, что это str, и поддерживает это.

Tuple и Set

Вы бы сделали то же самое, чтобы объявить tuple и set:

from typing import Set, Tuple


def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]):
    return items_t, items_s
🤓 Other versions and variants
def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
    return items_t, items_s

Это означает:

  • Переменная items_t является tuple с 3 элементами: int, другим int и str.
  • Переменная items_s является set и каждый элемент имеет тип bytes.

Dict

Чтобы определить dict, вы передаёте 2 параметра типов, разделённых запятыми.

Первый параметр типа предназначен для ключей dict.

Второй параметр типа предназначен для значений dict:

from typing import Dict


def process_items(prices: Dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)
🤓 Other versions and variants
def process_items(prices: dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)

Это означает:

  • Переменная prices является dict:
    • Ключи этого dict имеют тип str (скажем, название каждого элемента).
    • Значения этого dict имеют тип float (скажем, цена каждой позиции).

Optional

Вы также можете использовать Optional, чтобы объявить, что переменная имеет тип, например, str, но это является «необязательным», что означает, что она также может быть None:

from typing import Optional


def say_hi(name: Optional[str] = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")

Использование Optional[str] вместо просто str позволит редактору помочь вам в обнаружении ошибок, в которых вы могли бы предположить, что значение всегда является str, хотя на самом деле это может быть и None.

Generic-типы

Эти типы принимают параметры в квадратных скобках:

  • List
  • Tuple
  • Set
  • Dict
  • Optional
  • ...и др.

называются Generic-типами или Generics.

Классы как типы

Вы также можете объявить класс как тип переменной.

Допустим, у вас есть класс Person с полем name:

class Person:
    def __init__(self, name: str):
        self.name = name


def get_person_name(one_person: Person):
    return one_person.name

Тогда вы можете объявить переменную типа Person:

class Person:
    def __init__(self, name: str):
        self.name = name


def get_person_name(one_person: Person):
    return one_person.name

И снова вы получаете полную поддержку редактора:

Pydantic-модели

Pydantic является Python-библиотекой для выполнения валидации данных.

Вы объявляете «форму» данных как классы с атрибутами.

И каждый атрибут имеет тип.

Затем вы создаете экземпляр этого класса с некоторыми значениями, и он проверяет значения, преобразует их в соответствующий тип (если все верно) и предоставляет вам объект со всеми данными.

И вы получаете полную поддержку редактора для этого итогового объекта.

Взято из официальной документации Pydantic:

from datetime import datetime
from typing import List, Union

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: Union[datetime, None] = None
    friends: List[int] = []


external_data = {
    "id": "123",
    "signup_ts": "2017-06-01 12:22",
    "friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
🤓 Other versions and variants
from datetime import datetime

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: datetime | None = None
    friends: list[int] = []


external_data = {
    "id": "123",
    "signup_ts": "2017-06-01 12:22",
    "friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import Union

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: Union[datetime, None] = None
    friends: list[int] = []


external_data = {
    "id": "123",
    "signup_ts": "2017-06-01 12:22",
    "friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123

Info

Чтобы узнать больше о Pydantic, читайте его документацию.

FastAPI целиком основан на Pydantic.

Вы увидите намного больше всего этого на практике в Руководстве пользователя.

Аннотации типов в FastAPI

FastAPI получает преимущества аннотаций типов для выполнения определённых задач.

С FastAPI вы объявляете параметры с аннотациями типов и получаете:

  • Поддержку редактора.
  • Проверки типов.

...и FastAPI использует тот же механизм для:

  • Определения требований: из параметров пути запроса, параметров запроса, заголовков, зависимостей и т.д.
  • Преобразования данных: от запроса к нужному типу.
  • Валидации данных: исходя из каждого запроса:
    • Генерации автоматических ошибок, возвращаемых клиенту, когда данные не являются корректными.
  • Документирования API с использованием OpenAPI:
    • который затем используется пользовательскими интерфейсами автоматической интерактивной документации.

Всё это может показаться абстрактным. Не волнуйтесь. Вы увидите всё это в действии в Руководстве пользователя.

Важно то, что при использовании стандартных типов Python в одном месте (вместо добавления дополнительных классов, декораторов и т.д.) FastAPI сделает за вас большую часть работы.

Info

Если вы уже прошли всё руководство и вернулись, чтобы узнать больше о типах, хорошим ресурсом является «шпаргалка» от mypy.