← назад к разделу

PYTHON · FASTAPI Фаза 5. Фундамент - ядро FastAPI

Обязательно

Dependency Injection в FastAPI: Depends

Dependencies · Yield · Overrides

FastAPI собирает объекты по дереву зависимостей, кеширует их на запрос и позволяет подменить внешний ресурс в тесте.

1. Depends передаёт готовый объект

1 / 5

Обработчик объявляет тип и функцию-источник через Annotated. Настройки, сессия и пользователь приходят аргументами, а не читаются из глобальных переменных.

get_settings()обычная функцияDependsвызватьSettingsinfo(settings)endpoint

2. Дерево собирается снизу вверх

2 / 5

Сессия нужна репозиторию, репозиторий - обработчику сценария, сценарий - эндпоинту. FastAPI разрешает всю цепочку и кеширует каждую зависимость на запрос.

get_sessionget_repositoryget_handlerодна сессияна запрос

3. yield разделяет ресурс и очистку

3 / 5

До yield открывается сессия, после yield она закрывается даже при ошибке. Фабрика живёт в lifespan, а конкретная сессия - одну единицу работы.

async withopen sessionyield sessionendpoint uses itCleanupclose always

4. dependency_overrides подменяет границу

4 / 5

Тест связывает исходную get_session с тестовой фабрикой. Код эндпоинта не меняется, после теста overrides очищаются.

get_sessionget_test_sessionoverridesoriginal → fakeTestClientclear после

5. Зависимость собирает, но не решает

5 / 5

Тонкая функция создаёт объект и передаёт его дальше. Бизнес-правила остаются в сценарии, проверки без результата можно подключить ко всему роутеру.

Dependsоткуда взять объектcreate · yield · guardUse caseчто делать с объектомbusiness rules

Когда пишешь первый эндпоинт, кажется: просто прочитай из базы и верни. Но быстро появляются вопросы: где создавать подключение к базе? Как проверить токен? Как в тестах обойтись без реальной базы? Без ответа на эти вопросы каждый эндпоинт превращается в мешанину из разного кода.

FastAPI решает это через механизм Depends — встроенное внедрение зависимостей. Разберём, что это такое и зачем.

Зачем вообще нужен Depends

Представим простой эндпоинт: прочитать настройки из конфигурации и вернуть их.

Без Depends это выглядит так:

settings = Settings()  # создаём один раз глобально

@router.get("/info")
async def info():
    return {"debug": settings.debug}

Сейчас работает. Но как только нужно написать тест — приходится либо патчить глобальную переменную, либо переписывать код. А если настройки нужны в десяти эндпоинтах? А если кроме настроек — ещё сессия базы и текущий пользователь?

Depends решает это чисто: зависимость объявляется один раз как функция, FastAPI сам её вызывает и передаёт результат в эндпоинт.

Зависимость — обычная функция

Зависимость — это любая функция (или класс). Объявляется через Annotated[Тип, Depends(функция)]:

from typing import Annotated
from fastapi import Depends
from app.config import Settings, get_settings

@router.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {"debug": settings.debug}

get_settings — просто функция:

def get_settings() -> Settings:
    return Settings()

Стиль с Annotated читается явно: тип — это Settings, а откуда его взять — через get_settings. Чтобы не повторять одно и то же в каждом эндпоинте, зависимость выносят в псевдоним типа:

SettingsDep = Annotated[Settings, Depends(get_settings)]

@router.get("/info")
async def info(settings: SettingsDep):
    return {"debug": settings.debug}

Дерево зависимостей

Зависимость сама может зависеть от других. FastAPI строит дерево и разрешает его автоматически — сверху вниз:

эндпоинт create_product Depends get_handler Depends get_repository Depends get_session

FastAPI разрешает дерево сверху вниз сам; одна и та же зависимость вызывается один раз на запрос, сколько бы веток её ни просили.

def get_repository(session: SessionDep) -> ProductRepository:
    return ProductRepository(session)

def get_handler(
    repo: Annotated[ProductRepository, Depends(get_repository)],
) -> CreateProductHandler:
    return CreateProductHandler(repo)

@router.post("/products")
async def create_product(handler: Annotated[CreateProductHandler, Depends(get_handler)]):
    ...

Здесь session → repository → handler — три уровня, и FastAPI разберётся со всем сам.

Важная деталь: если одна и та же зависимость нужна нескольким местам в дереве, FastAPI вызовет её один раз на запрос и передаст один и тот же объект везде. Это называют кэшированием зависимостей. Именно поэтому сессию базы удобно объявить как зависимость — она будет одна на запрос, независимо от того, сколько мест её используют. Если нужен новый объект на каждое использование, кэш отключают точечно: Depends(get_x, use_cache=False).

yield-зависимости: ресурс с очисткой

Сессию базы нужно не только открыть, но и закрыть — даже если в эндпоинте произошла ошибка. Для этого существуют yield-зависимости.

Код до yield — подготовка ресурса. Код после yield — очистка. FastAPI гарантирует, что очистка выполнится после завершения запроса в любом случае:

from collections.abc import AsyncGenerator
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession

async def get_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
    async with request.app.state.session_factory() as session:
        yield session

SessionDep = Annotated[AsyncSession, Depends(get_session)]

async with закрывает сессию сразу после yield — это и есть очистка. Один сеанс живёт ровно один запрос. Фабрику сессий создаёт lifespan один раз на старте (см. структуру приложения) — делать новую async_sessionmaker на каждый запрос не нужно.

Такой же принцип работает для любого ресурса, который нужно освободить: файл, HTTP-клиент, соединение с очередью.

Переопределение зависимостей в тестах

Главная практическая выгода Depends — любую зависимость можно подменить в тестах, не трогая код эндпоинтов.

Делается через app.dependency_overrides: указываем, какую функцию-зависимость заменить и на что:

async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
    async with test_session_factory() as session:
        yield session

app.dependency_overrides[get_session] = get_test_session

Теперь в тестах эндпоинты работают с тестовой базой. Реальный код не изменился ни на строчку. Именно ради этого стоит проводить через Depends всё, что обращается наружу — подменяемость встроена с самого начала.

Зависимость — это сборка, а не логика

Важная граница: Depends отвечает на вопрос «откуда взять объект», а не «что с ним делать». Логика — в том коде, который получает этот объект, а не в самой зависимости.

Если в функцию-зависимость начинает проникать ветвление по бизнес-правилам — это сигнал, что она разрослась не туда. Зависимость остаётся тонкой: создала объект, передала — и всё.

Дополнительно: при первом чтении можно пропустить

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

Зависимость не обязана что-то возвращать. Проверку ключа API или лимита частоты вешают на роутер списком dependencies=[...]: она выполнится перед каждым эндпоинтом роутера, а в сигнатурах ничего не появится:

router = APIRouter(prefix="/admin", dependencies=[Depends(verify_api_key)])

Зависимостью может быть и класс: FastAPI прочитает его __init__ как сигнатуру и соберёт объект из параметров запроса. Annotated[Pagination, Depends()] без аргумента в Depends означает «создай сам этот класс» — удобно для повторяющихся наборов параметров вроде page и size.

Код после yield видит исключение из эндпоинта: FastAPI поднимает его внутри зависимости, так что откат транзакции пишут прямо там, а ошибку пробрасывают дальше:

async def get_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
    async with request.app.state.session_factory() as session:
        try:
            yield session
        except Exception:
            await session.rollback()
            raise

Важная деталь про момент очистки. В FastAPI с версии 0.118 код после yield выполняется после отправки ответа и после фоновых задач, поэтому сессию из зависимости можно использовать в BackgroundTasks. В версиях с 0.106 по 0.117 было наоборот: очистка шла до отправки ответа, и сессия к моменту фоновой задачи уже была закрыта. Если проект сидит на старой версии, в фоновую задачу передают идентификаторы, а не сессию.

Коротко

  • Depends — встроенный механизм внедрения зависимостей в FastAPI; зависимость — это просто функция.
  • FastAPI сам вызывает зависимость перед эндпоинтом и передаёт результат аргументом.
  • Зависимости образуют дерево: одна зависимость может зависеть от другой, FastAPI разрешает всё автоматически.
  • Одинаковая зависимость вызывается один раз на запрос и кэшируется — это безопасно использовать в нескольких местах.
  • yield-зависимость гарантирует очистку ресурса после запроса, даже при ошибке.
  • Любую зависимость можно подменить в тестах через app.dependency_overrides — это ключевая причина использовать Depends для всего, что работает с внешними ресурсами.
  • Зависимость без результата вешают на роутер через dependencies=[...]; класс тоже может быть зависимостью; код после yield видит исключение эндпоинта и с 0.118 выполняется после отправки ответа.

Что пощупать

В практикуме remodov/marketplace-system-python зависимости роутера сведены к одной: сервис лежит в app.state.products, а service_of(request) достаёт его. Тест кэша подменяет сервис в app.state на время теста и считает обращения к базе, не трогая ни роутер, ни Depends.

Код: router.py, main.py, test_cache.py.

Что почитать дальше