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

Введение в типы Python

Python поддерживает необязательные «подсказки типов» (их также называют «аннотациями типов»).

Эти «подсказки типов» или аннотации — это специальный синтаксис, позволяющий объявлять тип переменной.

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

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

FastAPI целиком основан на этих подсказках типов — они дают ему множество преимуществ и выгод.

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

Примечание

Если вы являетесь экспертом в 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. И внутренние значения тоже могут иметь свой тип.

Такие типы, которые содержат внутренние типы, называют «generic»-типами. И их можно объявлять, в том числе с указанием внутренних типов.

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

Новые версии Python

Синтаксис с использованием typing совместим со всеми версиями, от Python 3.6 до самых новых, включая Python 3.9, Python 3.10 и т.д.

По мере развития Python новые версии получают улучшенную поддержку этих аннотаций типов, и во многих случаях вам даже не нужно импортировать и использовать модуль typing, чтобы объявлять аннотации типов.

Если вы можете выбрать более свежую версию Python для проекта, вы получите дополнительную простоту.

Во всей документации есть примеры, совместимые с каждой версией Python (когда есть различия).

Например, «Python 3.6+» означает совместимость с Python 3.6 и выше (включая 3.7, 3.8, 3.9, 3.10 и т.д.). А «Python 3.9+» — совместимость с Python 3.9 и выше (включая 3.10 и т.п.).

Если вы можете использовать последние версии Python, используйте примеры для самой новой версии — у них будет самый лучший и простой синтаксис, например, «Python 3.10+».

List

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

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

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

Так как список — это тип, содержащий внутренние типы, укажите их в квадратных скобках:

def process_items(items: list[str]):
    for item in items:
        print(item)

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

from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)

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

В качестве типа используйте List, который вы импортировали из typing.

Так как список — это тип, содержащий внутренние типы, укажите их в квадратных скобках:

from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)

Информация

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

В данном случае str — это параметр типа, передаваемый в List (или list в Python 3.9 и выше).

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

Совет

Если вы используете Python 3.9 или выше, вам не нужно импортировать List из typing, можно использовать обычный встроенный тип list.

Таким образом, ваш редактор кода сможет помогать даже при обработке элементов списка:

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

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

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

Tuple и Set

Аналогично вы бы объявили tuple и set:

def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
    return items_t, items_s
from typing import Set, Tuple


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:

def process_items(prices: dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)
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)

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

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

Union

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

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

В Python 3.10 также появился новый синтаксис, где допустимые типы можно указать через вертикальную черту (|).

def process_item(item: int | str):
    print(item)
from typing import Union


def process_item(item: Union[int, str]):
    print(item)

В обоих случаях это означает, что item может быть int или str.

Возможно None

Вы можете объявить, что значение может иметь определённый тип, например str, но также может быть и None.

В Python 3.6 и выше (включая Python 3.10) это можно объявить, импортировав и используя Optional из модуля typing.

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.

Optional[Something] — это на самом деле сокращение для Union[Something, None], они эквивалентны.

Это также означает, что в Python 3.10 вы можете использовать Something | None:

def say_hi(name: str | None = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")
from typing import Optional


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


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

Использовать Union или Optional

Если вы используете версию Python ниже 3.10, вот совет с моей весьма субъективной точки зрения:

  • 🚨 Избегайте использования Optional[SomeType]
  • Вместо этого ✨ используйте Union[SomeType, None] ✨.

Оба варианта эквивалентны и внутри одинаковы, но я бы рекомендовал Union вместо Optional, потому что слово «optional» («необязательный») может навести на мысль, что значение необязательное, хотя на самом деле оно означает «может быть None», даже если параметр не является необязательным и всё ещё обязателен.

Мне кажется, Union[SomeType, None] более явно выражает смысл.

Речь только о словах и названиях. Но эти слова могут влиять на то, как вы и ваши коллеги думаете о коде.

В качестве примера возьмём эту функцию:

from typing import Optional


def say_hi(name: Optional[str]):
    print(f"Hey {name}!")
🤓 Other versions and variants
def say_hi(name: str | None):
    print(f"Hey {name}!")

Параметр name определён как Optional[str], но он не необязательный — вы не можете вызвать функцию без этого параметра:

say_hi()  # О нет, это вызывает ошибку! 😱

Параметр name всё ещё обязателен (не optional), потому что у него нет значения по умолчанию. При этом name принимает None как значение:

say_hi(name=None)  # Это работает, None допустим 🎉

Хорошая новость: как только вы перейдёте на Python 3.10, об этом можно не переживать — вы сможете просто использовать | для объединения типов:

def say_hi(name: str | None):
    print(f"Hey {name}!")
🤓 Other versions and variants
from typing import Optional


def say_hi(name: Optional[str]):
    print(f"Hey {name}!")

И тогда вам не придётся задумываться о названиях вроде Optional и Union. 😎

Generic-типы

Типы, которые принимают параметры типов в квадратных скобках, называются Generic-типами или Generics, например:

Вы можете использовать те же встроенные типы как generics (с квадратными скобками и типами внутри):

  • list
  • tuple
  • set
  • dict

И, как и в Python 3.8, из модуля typing:

  • Union
  • Optional (так же, как в Python 3.8)
  • ...и другие.

В Python 3.10, как альтернативу generics Union и Optional, можно использовать вертикальную черту (|) для объявления объединений типов — это гораздо лучше и проще.

Вы можете использовать те же встроенные типы как generics (с квадратными скобками и типами внутри):

  • list
  • tuple
  • set
  • dict

И, как и в Python 3.8, из модуля typing:

  • Union
  • Optional
  • ...и другие.
  • List
  • Tuple
  • Set
  • Dict
  • Union
  • Optional
  • ...и другие.

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

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

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

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

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

Обратите внимание, что это означает: «one_person — это экземпляр класса Person».

Это не означает: «one_person — это класс с именем Person».

Pydantic-модели

Pydantic — это библиотека Python для валидации данных.

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

И у каждого атрибута есть тип.

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

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

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

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
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

Информация

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

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

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

Совет

У Pydantic есть особое поведение, когда вы используете Optional или Union[Something, None] без значения по умолчанию. Подробнее читайте в документации Pydantic: Required Optional fields.

Подсказки типов с аннотациями метаданных

В Python также есть возможность добавлять дополнительные метаданные к подсказкам типов с помощью Annotated.

В Python 3.9 Annotated входит в стандартную библиотеку, поэтому вы можете импортировать его из typing.

from typing import Annotated


def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
    return f"Hello {name}"

В версиях ниже Python 3.9 импортируйте Annotated из typing_extensions.

Он уже будет установлен вместе с FastAPI.

from typing_extensions import Annotated


def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
    return f"Hello {name}"

Сам Python ничего не делает с Annotated. А для редакторов кода и других инструментов тип по-прежнему str.

Но вы можете использовать это место в Annotated, чтобы передать FastAPI дополнительные метаданные о том, как вы хотите, чтобы ваше приложение себя вело.

Важно помнить, что первый параметр типа, который вы передаёте в Annotated, — это фактический тип. Всё остальное — просто метаданные для других инструментов.

Пока вам достаточно знать, что Annotated существует и это — стандартный Python. 😎

Позже вы увидите, насколько это мощно.

Совет

Тот факт, что это стандартный Python, означает, что вы по-прежнему получите лучший возможный разработческий опыт в вашем редакторе кода, с инструментами для анализа и рефакторинга кода и т.д. ✨

А ещё ваш код будет очень совместим со множеством других инструментов и библиотек Python. 🚀

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

FastAPI использует эти подсказки типов для выполнения нескольких задач.

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

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

...и FastAPI использует эти же объявления для:

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

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

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

Информация

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