Когда пишешь первый эндпоинт, кажется: просто прочитай из базы и верни. Но быстро появляются вопросы: где создавать подключение к базе? Как проверить токен? Как в тестах обойтись без реальной базы? Без ответа на эти вопросы каждый эндпоинт превращается в мешанину из разного кода.
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 строит дерево и разрешает его автоматически — сверху вниз:
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.
Что почитать дальше
- Работа с базой данных: SQLAlchemy — как устроена сессия и транзакции в FastAPI.
- Безопасность и аутентификация — как через Depends реализуется проверка токенов.
- Тестирование FastAPI-приложений — подробнее о dependency_overrides и тестовых фикстурах.