Зависимости в FastAPI: рабочие приёмы и типичные грабли
Dependency Injection в FastAPI — не просто способ передать объект в эндпоинт. Разбираем приёмы, которые экономят код, и ошибки, которые превращают проект в неуправляемый клубок: вложенные цепочки, области жизни, тестируемость и реальную цену неправильного scope.

Почему привычный подход ломается
Типичный FastAPI-проект на старте обходится одной зависимостью — get_db, которая отдаёт сессию базы данных. Через полгода в каждом эндпоинте висит пять-шесть Depends(), половина из них дублируется, а интеграционные тесты падают из-за того, что кто-то забыл переопределить зависимость через app.dependency_overrides. Проблема не в механизме — он как раз лаконичен — а в том, как его масштабируют.
Ниже — четыре приёма, которые держат зависимости под контролем в проектах от десятка до сотни эндпоинтов, и разбор ошибок, которые встречаются чаще всего. Примеры для FastAPI 0.110+ и Python 3.11+.
Приём 1. Цепочки вместо плоского списка
Когда эндпоинту нужны и сессия БД, и текущий пользователь, и проверка прав, напрашивается три отдельных Depends(). Но пользователь без сессии не достаётся, а проверка прав бессмысленна без пользователя. Вместо того чтобы перечислять всё в сигнатуре, выстраивайте цепочку: каждая зависимость запрашивает предыдущую.
from fastapi import Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
# Зависимость первого уровня: сессия БД
async def get_db() -> AsyncSession: # type: ignore[misc]
async with async_session_maker() as session:
yield session
# Второй уровень: текущий пользователь
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
user = await user_repo.get_by_token(db, token)
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return user
# Третий уровень: проверка роли
def require_role(role: str):
async def _check(user: User = Depends(get_current_user)) -> User:
if role not in user.roles:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return user
return _check
Эндпоинт получает одну зависимость — Depends(require_role("editor")) — и ему не нужно знать ни про сессию, ни про токен. FastAPI сам разрешает цепочку и кеширует результат каждой зависимости внутри одного запроса: get_db вызовется ровно один раз, даже если на неё ссылаются две ветки графа.
Когда кеширование мешает
Кеширование работает по умолчанию: если два параметра ссылаются на одну и ту же функцию через Depends(), FastAPI вернёт один и тот же объект. Это экономит ресурсы, но иногда нужен свежий вызов — например, генератор случайного значения или отдельная транзакция. В таком случае передавайте use_cache=False:
from fastapi import Depends
async def fresh_nonce() -> str:
return secrets.token_hex(16)
@router.get("/double-nonce")
async def double_nonce(
a: str = Depends(fresh_nonce, use_cache=False),
b: str = Depends(fresh_nonce, use_cache=False),
) -> dict:
# a и b — разные значения
return {"a": a, "b": b}
Приём 2. Параметризованные зависимости через классы
Фабричная функция вроде require_role выше — рабочий вариант, но при нескольких параметрах замыкания становятся нечитаемыми. Класс с __call__ читается лучше и естественно хранит конфигурацию.
from fastapi import Depends, Query
class Pagination:
"""Зависимость для пагинации с ограничением максимума."""
def __init__(self, max_limit: int = 100):
self.max_limit = max_limit
def __call__(
self,
offset: int = Query(0, ge=0),
limit: int = Query(20, ge=1),
) -> dict:
# Не даём запросить больше max_limit
return {"offset": offset, "limit": min(limit, self.max_limit)}
# Для каталога — до 100 записей
catalog_pagination = Pagination(max_limit=100)
# Для админки — до 500
admin_pagination = Pagination(max_limit=500)
В эндпоинте: pages: dict = Depends(catalog_pagination). Параметризация очевидна, типы проверяются, документация в Swagger генерируется автоматически.
Приём 3. Группировка через роутер
Если все эндпоинты роутера требуют одну и ту же зависимость — не дублируйте её в каждой функции. У APIRouter есть параметр dependencies:
from fastapi import APIRouter, Depends
admin_router = APIRouter(
prefix="/admin",
tags=["admin"],
dependencies=[Depends(require_role("admin"))],
)
@admin_router.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
# Проверка роли уже произошла на уровне роутера
return await user_repo.list_all(db)
Зависимости роутера не возвращают значения в сигнатуру — они работают как guards. Если внутри нужна и проверка, и данные (например, объект пользователя), передавайте зависимость явно в функцию, а на роутер вешайте только побочные проверки вроде rate-limit.
Приём 4. Тестирование через dependency_overrides
Главное преимущество DI — подмена зависимостей в тестах без моков и патчей. FastAPI хранит словарь app.dependency_overrides, где ключ — оригинальная функция, значение — замена.
from httpx import AsyncClient, ASGITransport
import pytest
@pytest.fixture
def test_app():
app.dependency_overrides[get_db] = lambda: fake_db_session
app.dependency_overrides[get_current_user] = lambda: fake_user
yield app
app.dependency_overrides.clear() # Обязательно чистить
@pytest.mark.anyio
async def test_list_users(test_app):
transport = ASGITransport(app=test_app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/admin/users")
assert response.status_code == 200
Ловушка: если забыть .clear(), переопределение утечёт в следующий тест. В больших наборах это даёт «зелёные тесты поодиночке, красные вместе» — классический heisenbug.
Сравнение подходов к параметризации
| Подход | Читаемость | Типизация | Swagger | Когда выбирать |
|---|---|---|---|---|
| Замыкание (фабричная функция) | Средняя | Теряется без аннотаций | Работает | Один параметр, быстрый guard |
Класс с __call__ | Высокая | Полная | Работает | Несколько параметров, переиспользование |
Генератор (yield) | Высокая | Полная | Не отображает cleanup | Нужен teardown: сессия, файл, соединение |
Типичные ошибки
1. Зависимость без yield для ресурсов с жизненным циклом
Если get_db возвращает сессию через return, она никогда не закроется. Используйте yield — FastAPI гарантирует вызов кода после yield даже при исключении в эндпоинте. Это эквивалент try/finally.
2. Тяжёлая логика в зависимости верхнего уровня
Зависимости разрешаются последовательно по графу. Если get_current_user делает два запроса в базу и один во внешний сервис, каждый эндпоинт платит за это даже при кешировании. Выносите тяжёлые проверки в middleware или в отдельную зависимость, которую подключаете только там, где она нужна.
3. Циклические зависимости
FastAPI не проверяет циклы на старте — вы получите RecursionError в рантайме при первом запросе. Если A зависит от B, а B от A, выделите общую часть в третью зависимость C.
4. Зависимость, которая молча возвращает None
Генератор без yield — обычная функция, которая вернёт None. Линтер не подскажет, если тип не аннотирован. Всегда аннотируйте возвращаемый тип зависимости.
Зависимости в FastAPI — это граф, а не список. Как только вы начинаете думать о них как о дереве с кешированием на каждом узле, архитектура перестаёт расползаться.
Когда что выбирать
- Простой guard без возвращаемого значения (rate-limit, проверка заголовка) — зависимость на уровне роутера.
- Данные из запроса (пагинация, фильтры, сортировка) — класс с
__call__, параметризованный под контекст. - Ресурсы с жизненным циклом (БД-сессия, HTTP-клиент, файловый хендл) — генератор с
yield. - Цепочка авторизации (токен → пользователь → роль) — вложенные зависимости, каждая на своём уровне.
Смежные вопросы — lifespan-менеджер для ресурсов на всё время жизни приложения и middleware для сквозных задач вроде логирования — отдельная тема; здесь рассматриваем только per-request зависимости.
Сноска. Dependency Injection (DI) — паттерн, при котором объект не создаёт свои зависимости сам, а получает их извне. В FastAPI реализован через вызываемые объекты и параметр Depends(): фреймворк сам строит граф зависимостей, разрешает его для каждого запроса и передаёт результаты в аргументы функции-эндпоинта.
Что делать дальше
- Проверьте, нет ли в проекте зависимостей с
returnвместоyieldдля ресурсов — это самая частая утечка. - Замерьте, сколько времени занимает разрешение графа зависимостей на критичных эндпоинтах: добавьте middleware с таймингом до и после вызова.
- Переведите тесты на
dependency_overrides, если ещё используетеunittest.mock.patch— это устранит хрупкие патчи по путям импорта. - Изучите раздел Dependencies в официальной документации — там описаны sub-dependencies и advanced-сценарии с подробными примерами.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: FastAPI — официальная документация: Dependencies, FastAPI — Advanced Dependencies
Частые вопросы
Можно ли использовать синхронную функцию как зависимость в асинхронном эндпоинте?
Зависимость с yield выполнит код очистки, если эндпоинт выбросит исключение?
Как избежать дублирования Depends() во всех эндпоинтах роутера?
Что будет, если у меня другая версия FastAPI — приёмы из статьи сработают?
Не замедляет ли граф зависимостей обработку запроса?
Какие подводные камни dependency_overrides создаёт в проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.