Перейти к содержанию
Бэкенд и системы

37 вопросов по теме «Django и FastAPI» на собеседовании

В этом материале — 37 вопросов из русской колоды RecallDeck по теме «Django и FastAPI». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

33 мин чтения37 подробных ответовПроверено 24 августа 2026
Главная мысль

Стройте ответ вокруг владения состоянием, лимитов, отказа и восстановления. Определение становится инженерным ответом только в конкретном production-сценарии.

Вопросы и ответы

37 подробных ответов

01

Что такое WSGI и ASGI, и в чём разница?

Короткий ответ: WSGI (Web Server Gateway Interface) — синхронный стандарт интерфейса между веб-сервером и Python-приложением. ASGI (Asynchronous Server Gateway Interface) — его асинхронный преемник, поддерживающий async/await, WebSocket и long-polling. WSGI = одно соединение блокирует воркер; ASGI = один воркер обслуживает много соединений конкурентно.

Подробно:

WSGI определён в PEP 3333. Приложение — это вызываемый объект (environ, start_response) -> iterable:

# WSGI-приложение (синхронное)
def application(environ, start_response):
    status = "200 OK"
    headers = [("Content-Type", "text/plain")]
    start_response(status, headers)
    return [b"Hello WSGI"]

Модель обработки строго синхронная: воркер берёт запрос, выполняет его целиком (включая блокирующие I/O — БД, HTTP к внешним сервисам), и только потом берёт следующий. Под WSGI работают Django (классически), Flask.

ASGI — это async def app(scope, receive, send). scope описывает соединение (тип: http, websocket, lifespan), receive/send — асинхронные каналы событий:

# ASGI-приложение (асинхронное)
async def application(scope, receive, send):
    assert scope["type"] == "http"
    await send({
        "type": "http.response.start",
        "status": 200,
        "headers": [(b"content-type", b"text/plain")],
    })
    await send({
        "type": "http.response.body",
        "body": b"Hello ASGI",
    })

Под ASGI работают FastAPI, Starlette, современный Django (с async def views), Channels.

WSGI ASGI
Модель синхронная асинхронная (async/await)
Сигнатура (environ, start_response) async (scope, receive, send)
WebSocket нет да
Конкурентность через процессы/потоки через event loop + потоки/процессы
Серверы gunicorn, uWSGI uvicorn, hypercorn, daphne
Фреймворки Django, Flask FastAPI, Starlette, async Django

⚠️ Ловушка: ASGI не делает «обычный» (CPU-bound) код быстрее. Выигрыш только на I/O-bound задачах, где много времени тратится на ожидание. Если внутри async def view вызвать блокирующую функцию (например, синхронный драйвер БД или time.sleep), вы заблокируете весь event loop и убьёте конкурентность всех соединений на этом воркере.

02

Зачем нужен ASGI, если WSGI годами всех устраивал?

Короткий ответ: WSGI принципиально не поддерживает long-lived соединения (WebSocket, SSE) и асинхронный I/O. ASGI решает обе проблемы: позволяет одному воркеру держать тысячи открытых соединений и эффективно обслуживать I/O-bound нагрузку без раздувания числа процессов/потоков.

Подробно:

Проблемы WSGI:

  1. Один запрос = один занятый воркер. При 100 одновременных «медленных» запросах (например, ожидание ответа от внешнего API 2 сек) нужно ~100 воркеров/потоков. Каждый процесс ест память (десятки–сотни МБ).
  2. Нет WebSocket / SSE. Модель «запрос-ответ» завершается сразу, нет места долгоживущему соединению.
  3. Нет нативного async. Нельзя await внутри view.

С ASGI один event loop конкурентно обслуживает множество ожидающих I/O корутин:

# FastAPI: пока ждём внешний API, event loop обслуживает другие запросы
import httpx
from fastapi import FastAPI

app = FastAPI()

@app.get("/proxy")
async def proxy():
    async with httpx.AsyncClient() as client:
        r = await client.get("https://api.example.com/data")  # await => loop свободен
    return r.json()

Когда выбирать что:

  • CPU-bound, простой CRUD, мало конкурентных соединений → WSGI вполне достаточно.
  • Много медленного I/O, real-time (чаты, нотификации), стриминг, высокая конкурентность → ASGI.

⚠️ Ловушка: ASGI — не «серебряная пуля». Под ним легко получить деградацию, если случайно вызвать синхронный блокирующий код. В Django под ASGI ORM остаётся синхронным — обращения к БД оборачиваются в sync_to_async (через thread pool), и неаккуратное использование сводит на нет выигрыш.

03

Чем отличаются gunicorn и uvicorn? Как их комбинируют?

Короткий ответ: gunicorn — production WSGI-сервер с менеджером процессов-воркеров (pre-fork). uvicorn — лёгкий ASGI-сервер на uvloop/httptools. На практике часто запускают gunicorn как менеджер процессов с классом воркера uvicorn.workers.UvicornWorker, чтобы получить и надёжное управление процессами, и ASGI.

Подробно:

# Чистый WSGI (Django/Flask)
gunicorn myproject.wsgi:application --workers 5 --bind 0.0.0.0:8000

# Чистый ASGI (FastAPI) — uvicorn напрямую
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

# Лучшее из двух миров: gunicorn управляет, uvicorn обрабатывает ASGI
gunicorn main:app \
    --workers 4 \
    --worker-class uvicorn.workers.UvicornWorker \
    --bind 0.0.0.0:8000

Зачем gunicorn перед uvicorn: gunicorn даёт зрелое управление процессами — graceful reload, перезапуск зависших воркеров (--timeout), мониторинг. uvicorn-воркеры дают async-обработку.

Типы воркеров gunicorn (WSGI):

  • sync (по умолчанию) — 1 запрос на воркер, надёжно, для CPU-bound.
  • gthread — потоки внутри воркера (--threads N), для смешанной нагрузки.
  • gevent/eventlet — green threads, для I/O-bound на WSGI.

⚠️ Ловушка: Не запускайте uvicorn в --reload режиме в production — он для разработки и медленнее. Также не путайте --workers (процессы) с --threads (потоки внутри процесса): у async-воркеров потоки обычно не нужны, конкурентность даёт event loop.

04

Что такое MTV в Django и как это соотносится с MVC?

Короткий ответ: Django использует паттерн MTV — Model (данные/ORM), Template (представление/HTML), View (бизнес-логика, решает что показать). Это тот же MVC, но «Controller» в Django — это сам фреймворк (URL dispatcher), а «View» в MVC соответствует «Template» в Django.

Подробно:

MVC Django MTV Роль
Model Model Данные, ORM, бизнес-сущности
View Template Презентация (HTML/JSON)
Controller View Логика обработки запроса
(URLconf) Маршрутизация (роль «контроллера» MVC)
# Model — models.py
class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()

# View — views.py (это «контроллер» в терминах MVC)
from django.shortcuts import render

def article_detail(request, pk):
    article = Article.objects.get(pk=pk)
    return render(request, "article.html", {"article": article})

# Template — article.html
# <h1>{{ article.title }}</h1><p>{{ article.body }}</p>

⚠️ Ловушка: Частая путаница на собеседовании: в Django «View» — это НЕ то, что видит пользователь (как в MVC), а обработчик запроса. То, что видит пользователь, — это Template.

05

Опиши путь запроса в Django от сервера до ответа.

Короткий ответ: WSGI/ASGI сервер → WSGI/ASGI handler Django → стек middleware (вниз) → URL resolver → view → (ORM/template) → HttpResponse → стек middleware (вверх) → сервер → клиент.

Подробно:

  1. Сервер (gunicorn) получает HTTP-запрос, формирует environ, вызывает django.core.wsgi.get_wsgi_application().
  2. Django создаёт объект HttpRequest.
  3. Запрос проходит вниз через MIDDLEWARE (фаза «request» каждого middleware).
  4. URLResolver сопоставляет path с urlpatterns, находит view.
  5. View выполняет логику: запросы к ORM, рендер шаблона, формирование данных.
  6. View возвращает HttpResponse.
  7. Ответ проходит вверх через middleware в обратном порядке (фаза «response»).
  8. Сервер отдаёт ответ клиенту.
def my_view(request):
    # request — это HttpRequest
    name = request.GET.get("name", "World")
    return HttpResponse(f"Hello, {name}")  # это HttpResponse

⚠️ Ловушка: Если middleware вернёт response раньше (например, редирект на логин в auth middleware), нижележащие middleware и view вообще не выполнятся — это короткое замыкание. Важно понимать, на какой стадии каждый middleware прерывает цепочку.

06

Как работает маршрутизация URL в Django?

Короткий ответ: Список urlpatterns сопоставляет URL-паттерны с views. Используются path() (с конвертерами типов) и re_path() (regex). URLconf можно вкладывать через include() для модульности приложений.

Подробно:

# project/urls.py
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),  # делегируем приложению
]

# blog/urls.py
from django.urls import path, re_path
from . import views

urlpatterns = [
    path("", views.index, name="blog-index"),
    path("<int:pk>/", views.detail, name="blog-detail"),  # конвертер int
    path("<slug:slug>/", views.by_slug),                  # конвертер slug
    re_path(r"^archive/(?P<year>[0-9]{4})/$", views.archive),
]

name= позволяет реверсировать URL без хардкода:

from django.urls import reverse
url = reverse("blog-detail", kwargs={"pk": 42})  # "/blog/42/"
# В шаблоне: {% url 'blog-detail' pk=42 %}

⚠️ Ловушка: Порядок паттернов важен — Django берёт первое совпадение. <slug:slug> поставленный раньше <int:pk>/ может перехватить запросы. Также не забывайте слеши: настройка APPEND_SLASH делает редирект, но только для GET, и сломает POST без завершающего слеша.

07

Function-Based Views vs Class-Based Views — что выбрать?

Короткий ответ: FBV (function-based) — простые функции (request) -> response, явные и читаемые. CBV (class-based) — классы с методами get/post, дают переиспользование через наследование и mixins, generic views сокращают шаблонный код для CRUD.

Подробно:

# FBV
from django.shortcuts import render, get_object_or_404

def article_detail(request, pk):
    article = get_object_or_404(Article, pk=pk)
    return render(request, "detail.html", {"article": article})

# CBV — generic
from django.views.generic import DetailView, ListView

class ArticleDetail(DetailView):
    model = Article
    template_name = "detail.html"

class ArticleList(ListView):
    model = Article
    paginate_by = 20

# urls.py: path("<int:pk>/", ArticleDetail.as_view())
  • FBV плюсы: простота, явность, легко читать; декораторы (@login_required) очевидны.
  • CBV плюсы: DRY для типового CRUD, mixins (LoginRequiredMixin, PermissionRequiredMixin), переопределение методов (get_queryset, get_context_data).
  • CBV минусы: «магия» наследования, сложно проследить, откуда метод (MRO).

⚠️ Ловушка: В CBV декораторы вешаются не напрямую, а через method_decorator или mixins. @login_required def get(...) на методе не сработает как ожидается — нужно @method_decorator(login_required, name="dispatch") на классе или LoginRequiredMixin.

08

Как устроен middleware в Django и почему важен порядок?

Короткий ответ: Middleware — это слои-обёртки вокруг view, обрабатывающие каждый request/response. Они вызываются вниз по списку MIDDLEWARE на входе и вверх (в обратном порядке) на выходе. Современный стиль — callable-класс, оборачивающий get_response.

Подробно:

class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response  # вызывается один раз при старте

    def __call__(self, request):
        # --- код ДО view (фаза request, порядок сверху вниз) ---
        import time
        start = time.monotonic()

        response = self.get_response(request)  # вызов следующего слоя / view

        # --- код ПОСЛЕ view (фаза response, порядок снизу вверх) ---
        response["X-Elapsed-Ms"] = int((time.monotonic() - start) * 1000)
        return response

Дополнительные хуки: process_view, process_exception, process_template_response.

Типичный порядок (важно!):

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",   # до auth
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware", # нужен session
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

⚠️ Ловушка: AuthenticationMiddleware зависит от SessionMiddleware — если поставить выше, request.user будет недоступен. Порядок — это и есть контракт зависимостей между слоями.

09

Объясни концепцию middleware и почему порядок имеет значение.

Короткий ответ: Middleware — это цепочка обёрток (паттерн «луковица»/decorator chain) вокруг бизнес-логики, реализующая сквозную функциональность (логирование, auth, CORS, сжатие, безопасность). Порядок важен, потому что request идёт сквозь слои в одном направлении, а response — в обратном, и слои зависят от данных, выставленных предыдущими.

Подробно:

Модель «луковицы» (onion): запрос «проникает» внутрь через слои, view в центре, ответ «выходит» через те же слои в обратном порядке.

request  → [Security] → [Session] → [Auth] → [CSRF] → VIEW
response ← [Security] ← [Session] ← [Auth] ← [CSRF] ← VIEW

Это концепция, общая для всех фреймворков:

  • Django: список MIDDLEWARE.
  • FastAPI/Starlette: app.add_middleware(...) — добавленные позже оборачивают добавленных раньше (внешний слой = последний добавленный).
  • Flask: WSGI middleware + before_request/after_request хуки.

Почему порядок критичен:

  • Auth middleware должен идти после session middleware (читает сессию).
  • GZip middleware должен сжимать уже готовый ответ — обычно ближе к внешнему слою.
  • CORS должен отрабатывать рано, чтобы preflight-запросы не доходили до тяжёлой логики.

⚠️ Ловушка: В FastAPI/Starlette порядок инверсный относительно интуиции: middleware, добавленный последним через add_middleware, выполняется ПЕРВЫМ для запроса (он самый внешний). Легко перепутать с Django, где первый в списке — самый внешний.

10

Как организованы settings и apps в Django?

Короткий ответ: settings.py — единый модуль конфигурации (БД, INSTALLED_APPS, middleware, секреты). App — самодостаточный модуль функциональности (models.py, views.py, migrations/), регистрируемый в INSTALLED_APPS. Проект состоит из множества apps.

Подробно:

# settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "rest_framework",   # сторонние
    "blog.apps.BlogConfig",  # своё приложение
]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "mydb",
        "HOST": os.environ["DB_HOST"],
    }
}
SECRET_KEY = os.environ["SECRET_KEY"]  # из env, не хардкод!
DEBUG = os.environ.get("DEBUG", "0") == "1"

Для разных окружений — разбивают settings: base.py, dev.py, prod.py и указывают DJANGO_SETTINGS_MODULE.

# blog/apps.py
from django.apps import AppConfig

class BlogConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "blog"

    def ready(self):
        import blog.signals  # регистрация сигналов

⚠️ Ловушка: DEBUG = True в production — серьёзная уязвимость: показывает stack traces с переменными окружения, и ALLOWED_HOSTS игнорируется. Также SECRET_KEY в репозитории = компрометация сессий и токенов.

11

Объясни миграции и ленивость QuerySet в Django ORM.

Короткий ответ: Миграции — версионируемые Python-файлы, описывающие изменения схемы БД (makemigrations генерирует, migrate применяет). QuerySet ленивый — SQL не выполняется при создании, только при первом обращении к данным (итерация, len, slice, list(), bool).

Подробно:

# Миграции
# python manage.py makemigrations  -> создаёт 0002_xxx.py
# python manage.py migrate          -> применяет к БД
# python manage.py showmigrations   -> статус

# Ленивость
qs = Article.objects.filter(published=True)  # SQL НЕ выполнен
qs = qs.exclude(draft=True)                   # всё ещё НЕ выполнен (chaining)
qs = qs.order_by("-created")                  # НЕ выполнен

for a in qs:   # <-- ВОТ ТУТ выполняется SQL (evaluation)
    print(a.title)

print(len(qs))   # уже закэшировано, нового запроса нет

Что триггерит выполнение: итерация, list(qs), len(qs), bool(qs), slicing с шагом, repr().

select_related загружает ForeignKey/OneToOne через SQL JOIN, а prefetch_related выполняет второй запрос для many-to-many и обратных связей и объединяет результаты в Python. Индексы должны соответствовать реальным фильтрам и сортировкам QuerySet; transaction.atomic() задаёт явную транзакционную границу для нескольких зависимых записей.

⚠️ Ловушка: qs.count() делает SELECT COUNT(*), а len(qs) загружает ВСЕ объекты в память и считает их. Для проверки «есть ли записи» используйте qs.exists() (эффективный SELECT 1 ... LIMIT 1), а не if qs: или len(qs).

12

Как бороться с N+1 запросами в Django?

Короткий ответ: select_related — для ForeignKey/OneToOne, делает SQL JOIN (один запрос). prefetch_related — для ManyToMany/обратных FK, делает отдельный запрос и джойнит в Python. Оба устраняют проблему N+1.

Подробно:

# N+1 ПРОБЛЕМА: 1 запрос на статьи + N запросов на авторов
for article in Article.objects.all():
    print(article.author.name)   # отдельный SQL на каждой итерации

# select_related — FK/O2O, через JOIN
for article in Article.objects.select_related("author"):
    print(article.author.name)   # всё в одном запросе

# prefetch_related — M2M / обратные связи
for article in Article.objects.prefetch_related("tags"):
    print([t.name for t in article.tags.all()])  # 2 запроса всего

Подробный разбор (когда что, Prefetch-объекты, only/defer, аннотации) — в файле про Django ORM в этой же директории.

⚠️ Ловушка: select_related по M2M невозможен (нельзя JOIN-нуть множество строк в одну) — для M2M только prefetch_related. Чрезмерный select_related с глубокими цепочками порождает огромные JOIN и дублирование данных.

13

Как работают формы и валидация в Django?

Короткий ответ: forms.Form/forms.ModelForm декларативно описывают поля, валидируют входные данные (is_valid()), очищают их (cleaned_data) и рендерят HTML. ModelForm автоматически строит поля из модели. Валидация — на уровне поля (clean_<field>) и формы (clean).

Подробно:

from django import forms

class ContactForm(forms.Form):
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea, max_length=2000)

    def clean_message(self):          # валидация одного поля
        data = self.cleaned_data["message"]
        if "spam" in data.lower():
            raise forms.ValidationError("No spam allowed")
        return data

    def clean(self):                   # кросс-полевая валидация
        cleaned = super().clean()
        # ... сравнение нескольких полей
        return cleaned

# ModelForm — поля из модели
class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "body"]

# во view
def view(request):
    form = ContactForm(request.POST or None)
    if request.method == "POST" and form.is_valid():
        send_email(form.cleaned_data)  # данные уже очищены и типизированы

⚠️ Ловушка: Обращаться к form.cleaned_data до вызова form.is_valid() нельзя — атрибута ещё нет. И в clean_<field> обязательно return значение — иначе поле станет None в cleaned_data.

14

Что такое Django admin и как его кастомизировать?

Короткий ответ: Django admin — автогенерируемая CRUD-панель для моделей, построенная по INSTALLED_APPS. Модели регистрируются через admin.site.register или декоратор @admin.register, поведение настраивается через ModelAdmin (списки, фильтры, поиск, inline).

Подробно:

from django.contrib import admin

class CommentInline(admin.TabularInline):
    model = Comment
    extra = 1

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ("title", "author", "published", "created")
    list_filter = ("published", "created")
    search_fields = ("title", "body")
    raw_id_fields = ("author",)        # вместо тяжёлого dropdown
    inlines = [CommentInline]
    readonly_fields = ("created",)

Admin отлично подходит для внутренних инструментов и работы контент-менеджеров.

⚠️ Ловушка: Admin — это полноценный доступ к данным с правами на запись/удаление. Не давайте админ-доступ кому попало и не выставляйте /admin/ без ограничений (rate-limit, нестандартный путь, 2FA). Также list_display с методом, дёргающим связанную модель, легко создаёт N+1 — используйте list_select_related.

15

Что такое signals в Django и почему с ними надо быть осторожным?

Короткий ответ: Signals — механизм publish/subscribe: отправитель шлёт сигнал (post_save, pre_delete, m2m_changed), а подписчики-ресиверы реагируют. Удобно для слабой связанности, но опасно: скрытая логика, проблемы с порядком, сложность отладки и тестирования.

Подробно:

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

Почему осторожно:

  1. Неявность. Логика выполняется «магически» при сохранении — не видно из кода, который вызвал save(). Тяжело отлаживать.
  2. Порядок и каскад. Несколько ресиверов на один сигнал, сигнал внутри ресивера → каскад, бесконечные циклы.
  3. bulk_create/update() не шлют сигналы. Model.objects.update(...) и bulk_create НЕ триггерят post_save — частый источник багов.
  4. Транзакции. Ресивер на post_save выполняется внутри транзакции; если он шлёт письмо/ставит Celery-задачу, а транзакция откатится — будет рассинхрон. Используйте transaction.on_commit.

Альтернатива: явный вызов метода/сервиса вместо сигнала, когда логика однозначна.

⚠️ Ловушка: Profile.objects.create() внутри post_save для User снова триггерит post_save (для Profile) — следите за бесконечной рекурсией. И помните: queryset.update() обходит и save(), и сигналы.

16

Какие механизмы безопасности Django даёт из коробки?

Короткий ответ: Django по умолчанию защищает от CSRF (токены в формах), XSS (автоэкранирование в шаблонах), SQL-инъекций (параметризованные запросы ORM), clickjacking (X-Frame-Options), и поддерживает безопасное хранение паролей (PBKDF2/Argon2), HTTPS-настройки.

Подробно:

# CSRF: токен обязателен для POST-форм
# в шаблоне: <form method="post">{% csrf_token %} ... </form>
# CsrfViewMiddleware проверяет токен; иначе 403

# XSS: автоэкранирование в Django Templates
# {{ user_input }} -> < превращается в &lt; автоматически
# Отключение опасно: {{ user_input|safe }} или mark_safe()

# SQL injection: ORM параметризует запросы
Article.objects.filter(title=user_input)            # безопасно (placeholder)
Article.objects.raw("SELECT * FROM a WHERE t=%s", [user_input])  # безопасно
# ОПАСНО: .raw(f"... WHERE t='{user_input}'")  — инъекция!

# Clickjacking
MIDDLEWARE += ["django.middleware.clickjacking.XFrameOptionsMiddleware"]
X_FRAME_OPTIONS = "DENY"

# Production-настройки
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000

⚠️ Ловушка: Фильтр |safe и mark_safe() отключают XSS-защиту — никогда не применяйте к пользовательскому вводу. И raw()/extra()/.raw() с f-строками сводят на нет защиту от SQL-инъекций — всегда передавайте параметры списком, а не интерполяцией.

17

В чём разница между Serializer и ModelSerializer в DRF?

Короткий ответ: Serializer — ручное декларативное описание полей и (де)сериализации/валидации, полный контроль. ModelSerializer — генерирует поля автоматически из модели и даёт готовые create/update, экономя код для типового CRUD.

Подробно:

from rest_framework import serializers

# Ручной Serializer
class ArticleSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    body = serializers.CharField()

    def create(self, validated_data):
        return Article.objects.create(**validated_data)

    def validate_title(self, value):       # валидация поля
        if Article.objects.filter(title=value).exists():
            raise serializers.ValidationError("Title must be unique")
        return value

# ModelSerializer — поля и create/update автоматически
class ArticleModelSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source="author.name", read_only=True)

    class Meta:
        model = Article
        fields = ["id", "title", "body", "author_name"]
        read_only_fields = ["id"]

Сериализатор работает в обе стороны: serializer.data (объект → dict/JSON) и serializer.is_valid() + serializer.validated_data (входной dict → проверенные данные).

⚠️ Ловушка: fields = "__all__" в ModelSerializer — анти-паттерн для API: при добавлении поля в модель оно автоматически утечёт в API (например, is_admin, password_hash). Указывайте поля явно. Также вложенные сериализаторы по умолчанию read-only — для записи нужен кастомный create/update.

18

Что такое ViewSet и router в DRF?

Короткий ответ: ViewSet группирует логику для набора связанных эндпоинтов (list/create/retrieve/update/destroy) в одном классе. Router автоматически генерирует URL-маршруты под этот ViewSet, избавляя от ручного описания urlpatterns.

Подробно:

from rest_framework import viewsets
from rest_framework.routers import DefaultRouter
from rest_framework.decorators import action
from rest_framework.response import Response

class ArticleViewSet(viewsets.ModelViewSet):   # CRUD из коробки
    queryset = Article.objects.select_related("author")
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

    @action(detail=True, methods=["post"])      # доп. эндпоинт /articles/{pk}/publish/
    def publish(self, request, pk=None):
        article = self.get_object()
        article.published = True
        article.save()
        return Response({"status": "published"})

# urls.py
router = DefaultRouter()
router.register(r"articles", ArticleViewSet)   # генерирует все маршруты
urlpatterns = router.urls

Иерархия: APIView (база) → GenericAPIView + mixins → generic views → ViewSet/ModelViewSet.

⚠️ Ловушка: ModelViewSet даёт сразу все операции, включая DELETE и PUT/PATCH. Если нужны только чтение — используйте ReadOnlyModelViewSet или ограничьте http_method_names, иначе случайно откроете запись/удаление.

19

Как устроены authentication и permission classes в DRF?

Короткий ответ: Authentication classes определяют, КТО делает запрос (заполняют request.user). Permission classes определяют, МОЖНО ли ему выполнить действие. Сначала проходит аутентификация, потом проверка прав. Оба настраиваются глобально или на уровне view.

Подробно:

# settings.py — глобально
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}

# на уровне view
from rest_framework.permissions import IsAuthenticated, BasePermission

class IsOwner(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.author == request.user

class ArticleViewSet(viewsets.ModelViewSet):
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated, IsOwner]

Готовые permissions: AllowAny, IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly, DjangoModelPermissions.

Два уровня проверки прав: has_permission (на весь запрос) и has_object_permission (на конкретный объект, вызывается из get_object).

⚠️ Ловушка: has_object_permission НЕ вызывается автоматически для list-эндпоинтов и для объектов, которые вы не получаете через get_object() — для list нужно фильтровать queryset вручную (get_queryset). Иначе пользователь увидит чужие записи в списке, хотя по объекту прав нет.

20

Как настроить пагинацию и rate limiting в DRF?

Короткий ответ: Pagination разбивает большие списки на страницы (PageNumberPagination, LimitOffsetPagination, CursorPagination). Throttling ограничивает частоту запросов (по пользователю/анониму/scope) для защиты от злоупотреблений.

Подробно:

REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,

    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "100/day",
        "user": "1000/day",
        "uploads": "10/min",   # scoped throttle
    },
}

# scoped throttle на конкретном view
class UploadView(APIView):
    throttle_scope = "uploads"

Типы пагинации:

  • PageNumberPagination?page=2, просто, но OFFSET медленный на больших данных.
  • LimitOffsetPagination?limit=20&offset=40.
  • CursorPagination — стабильна при вставках, эффективна на больших таблицах (по индексированному полю).

⚠️ Ловушка: PageNumberPagination/LimitOffsetPagination используют SQL OFFSET, который на больших таблицах медленный (БД сканирует и отбрасывает пропускаемые строки) и нестабилен при вставках/удалениях (сдвиг страниц). Для больших объёмов и инфинит-скролла — CursorPagination.

21

На чём построен FastAPI и почему он async-first?

Короткий ответ: FastAPI = Starlette (ASGI-фреймворк: роутинг, middleware, WebSocket) + Pydantic (валидация и сериализация через type hints). Async-first, потому что Starlette построен на ASGI; type hints дают валидацию, документацию и автокомплит «бесплатно».

Подробно:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):           # Pydantic — валидация по type hints
    name: str
    price: float
    in_stock: bool = True

@app.post("/items/")
async def create_item(item: Item) -> Item:   # async + типизированный body
    return item

Что даёт стек:

  • Starlette — ASGI-маршрутизация, middleware, фоновые задачи, WebSocket, тестовый клиент.
  • Pydantic — парсинг/валидация входа, сериализация выхода, генерация JSON Schema → OpenAPI.

Поддерживаются и def, и async def views: синхронные функции FastAPI выполняет в отдельном thread pool, чтобы не блокировать event loop.

⚠️ Ловушка: Если объявить async def, но внутри вызвать блокирующую операцию (синхронный SQLAlchemy, requests, time.sleep), вы заблокируете event loop. Либо используйте async-библиотеки (httpx, async-драйвер БД), либо объявляйте view как обычный def (тогда FastAPI сам уведёт его в thread pool).

22

Как FastAPI различает path, query и body параметры? Что такое response_model?

Короткий ответ: FastAPI выводит источник параметра из его типа и сигнатуры: переменная из пути → path param; простой тип не в пути → query param; Pydantic-модель → request body. response_model задаёт схему ответа: валидирует, фильтрует и документирует выход.

Подробно:

from fastapi import FastAPI, Query, Path
from pydantic import BaseModel

app = FastAPI()

class ItemIn(BaseModel):
    name: str
    password: str

class ItemOut(BaseModel):     # без password — он отфильтруется
    name: str

@app.get("/items/{item_id}")
async def read_item(
    item_id: int = Path(..., gt=0),               # path param, > 0
    q: str | None = Query(None, max_length=50),   # query param ?q=
    limit: int = 10,                              # query param с дефолтом
):
    return {"item_id": item_id, "q": q, "limit": limit}

@app.post("/items/", response_model=ItemOut)      # ответ ограничен схемой ItemOut
async def create(item: ItemIn):
    save(item)
    return item   # password в ответ НЕ попадёт — отфильтрован response_model

response_model — мощный инструмент безопасности: даже если вернуть лишние поля, в ответ попадут только описанные в модели.

⚠️ Ловушка: Без response_model (или -> ReturnType) FastAPI вернёт всё, что вернула функция, включая чувствительные поля (как is_admin, хэш пароля). Всегда задавайте явную выходную модель для эндпоинтов, отдающих данные.

23

Как работает Dependency Injection через Depends в FastAPI?

Короткий ответ: Depends объявляет зависимость — функцию/класс, которую FastAPI вызовет и результат подставит в параметр. Зависимости могут быть вложенными, кэшируются в рамках запроса, поддерживают yield для setup/teardown (например, сессия БД).

Подробно:

from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session

app = FastAPI()

# зависимость с teardown через yield
def get_db():
    db = SessionLocal()
    try:
        yield db          # значение подставляется в параметр
    finally:
        db.close()        # выполнится после ответа

# зависимость, использующая другую зависимость
def get_current_user(db: Session = Depends(get_db), token: str = Header(...)):
    user = db.query(User).filter_by(token=token).first()
    if not user:
        raise HTTPException(status_code=401)
    return user

@app.get("/me")
async def me(user: User = Depends(get_current_user)):
    return user

# зависимость уровня всего роутера / приложения
app = FastAPI(dependencies=[Depends(verify_api_key)])

Преимущества: переиспользование, тестируемость (через app.dependency_overrides), декларативность, автоматическое отображение в OpenAPI.

⚠️ Ловушка: Зависимость кэшируется в рамках ОДНОГО запроса (если одна и та же Depends встречается несколько раз, функция вызовется один раз). Чтобы отключить кэш — Depends(func, use_cache=False). Также yield-часть после yield выполняется уже после отправки ответа — не рассчитывайте там менять response.

24

Объясни идею Dependency Injection и зачем она нужна.

Короткий ответ: DI — это передача компоненту его зависимостей извне, а не создание их внутри. Это снижает связанность, упрощает замену реализаций и тестирование (можно подменить зависимость моком). FastAPI делает DI декларативным через Depends.

Подробно:

# БЕЗ DI: зависимость зашита внутри — тяжело тестировать
def get_user():
    db = ProductionDatabase()      # жёсткая связь
    return db.fetch_user()

# С DI: зависимость передаётся снаружи
def get_user(db: Database = Depends(get_db)):
    return db.fetch_user()

# В тесте подменяем реальную БД на фейк
app.dependency_overrides[get_db] = lambda: FakeDatabase()

Зачем:

  • Тестируемость — подмена реальных сервисов моками.
  • Слабая связанность — код зависит от абстракции, не от конкретной реализации.
  • Переиспользование — общая логика (auth, БД-сессия, пагинация) выносится в зависимость.
  • Управление жизненным цикломyield открывает/закрывает ресурсы.

Аналоги в других фреймворках: Django делает это менее явно (settings, middleware, request.user); Flask — через flask.g и current_app.

⚠️ Ловушка: DI ≠ глобальный singleton. Распространённая ошибка — создавать тяжёлый объект (например, HTTP-клиент) в зависимости на каждый запрос вместо переиспользования. Для дорогих ресурсов используйте lifespan-события или модульный singleton, а через Depends отдавайте уже созданный объект.

25

Как FastAPI генерирует документацию?

Короткий ответ: Из type hints и Pydantic-моделей FastAPI автоматически строит OpenAPI-схему (JSON Schema). По ней доступны интерактивные UI: Swagger UI (/docs) и ReDoc (/redoc), а также сырой /openapi.json.

Подробно:

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(title="My API", version="1.0.0")

class Item(BaseModel):
    name: str = Field(..., description="Название товара", examples=["Книга"])
    price: float = Field(..., gt=0, description="Цена > 0")

@app.post("/items/", summary="Создать товар", tags=["items"])
async def create(item: Item):
    """Создаёт новый товар. Текст docstring попадает в описание эндпоинта."""
    return item

Доступно автоматически:

  • /docs — Swagger UI (интерактивный, можно слать запросы).
  • /redoc — ReDoc (чистая документация).
  • /openapi.json — спецификация для генерации клиентов, импорта в Postman и т.п.

⚠️ Ловушка: В production интерактивные доки часто отключают или закрывают аутентификацией (docs_url=None, redoc_url=None), чтобы не раскрывать структуру API. Также Swagger показывает примеры запросов — не оставляйте в examples реальные секреты/токены.

26

Что такое BackgroundTasks и когда их использовать?

Короткий ответ: BackgroundTasks позволяет выполнить функцию ПОСЛЕ отправки ответа клиенту, не блокируя его (отправка письма, лог, инвалидация кэша). Это легковесная альтернатива Celery для коротких задач, не критичных к надёжности.

Подробно:

from fastapi import BackgroundTasks, FastAPI

app = FastAPI()

def write_log(message: str):
    with open("log.txt", "a") as f:
        f.write(message + "\n")

@app.post("/send/")
async def send(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(write_log, f"sent to {email}")
    return {"status": "queued"}   # ответ уходит сразу, лог пишется после

Когда BackgroundTasks, а когда Celery:

  • BackgroundTasks — короткие, in-process, не критичные задачи. Выполняются в том же процессе; если процесс упадёт — задача потеряется.
  • Celery — долгие, тяжёлые, требующие надёжности/ретраев/распределения по воркерам.

⚠️ Ловушка: BackgroundTasks выполняются в том же процессе/воркере — тяжёлая задача (CPU-bound, минутный экспорт) заблокирует воркер и съест его ресурсы. Для надёжности (гарантия выполнения, ретраи) и тяжёлых задач — только полноценная очередь (Celery/RQ/ARQ).

27

За счёт чего FastAPI считается быстрым?

Короткий ответ: За счёт ASGI + async I/O (один воркер держит много конкурентных запросов), быстрого ASGI-сервера (uvicorn на uvloop/httptools, частично на C), и валидации Pydantic v2 (ядро на Rust). FastAPI близок к производительности Node.js/Go на I/O-bound нагрузке.

Подробно:

Слагаемые скорости:

  1. ASGI + asyncio — один event loop конкурентно обслуживает множество I/O-bound запросов, пока они ждут сеть или БД; синхронный блокирующий код в async def уничтожает это преимущество.
  2. uvicorn / uvloop — uvloop — это замена asyncio event loop на базе libuv (того же, что в Node.js), httptools — быстрый HTTP-парсер на C.
  3. Pydantic v2 — валидационное ядро pydantic-core написано на Rust, в разы быстрее v1 (чистый Python).
  4. Минимальные накладные расходы — Starlette тонкий, нет тяжёлого ORM/admin в ядре.
# Конкурентность: пока ждём 3 внешних запроса, loop их совмещает
import asyncio, httpx

@app.get("/aggregate")
async def aggregate():
    async with httpx.AsyncClient() as c:
        a, b, d = await asyncio.gather(   # параллельно, не последовательно
            c.get("https://s1/"), c.get("https://s2/"), c.get("https://s3/"),
        )
    return {"s1": a.json(), "s2": b.json(), "s3": d.json()}

⚠️ Ловушка: «FastAPI быстрый» относится к I/O-bound сценариям. На чистом CPU-bound коде Python остаётся Python (GIL), и async не помогает — для CPU-нагрузки нужны процессы (несколько воркеров) или вынос в Celery/процесс-пул. Сравнения «FastAPI vs Flask» по голому «hello world» вводят в заблуждение.

28

Что такое Pydantic и чем v2 отличается от v1?

Короткий ответ: Pydantic — библиотека валидации/сериализации данных по Python type hints через BaseModel. v2 переписана с ядром на Rust (pydantic-core) — в разы быстрее, с новым API валидаторов (field_validator, model_validator) и отдельным пакетом pydantic-settings для конфигурации.

Подробно:

from pydantic import BaseModel, field_validator, model_validator, EmailStr, Field

class User(BaseModel):
    name: str = Field(..., min_length=1)
    email: EmailStr
    age: int = Field(..., ge=0, le=120)

    @field_validator("name")          # v2 (в v1 было @validator)
    @classmethod
    def name_titlecase(cls, v: str) -> str:
        return v.title()

    @model_validator(mode="after")    # кросс-полевая валидация
    def check(self):
        if self.age < 18 and self.name == "admin":
            raise ValueError("admin must be adult")
        return self

u = User(name="ann", email="a@b.com", age="30")  # "30" приводится к int 30
# u.name == "Ann"

Ключевые отличия v1 → v2:

  • Ядро на Rust → производительность.
  • @validator@field_validator, @root_validator@model_validator.
  • .dict().model_dump(), .json().model_dump_json(), .parse_obj().model_validate().
  • class Configmodel_config = ConfigDict(...).
  • Settings вынесены в отдельный пакет pydantic-settings.
# Конфигурация приложения из env
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    db_url: str
    debug: bool = False
    model_config = {"env_file": ".env"}

settings = Settings()   # читает из переменных окружения / .env

⚠️ Ловушка: В v2 BaseSettings переехал из pydantic в pydantic_settings — частая ошибка миграции (ImportError). И помните, что v2 по умолчанию делает «мягкое» приведение типов ("30"30); если нужна строгость — Field(strict=True) или StrictInt.

29

Что такое Flask, и что такое app/request context и blueprints?

Короткий ответ: Flask — микрофреймворк (WSGI) на базе Werkzeug (HTTP/маршрутизация) и Jinja2 (шаблоны). Даёт минимум из коробки, остальное — через расширения. Blueprints модуляризуют приложение. Application context и request context — механизмы доступа к глобальным объектам (current_app, request, g) в рамках запроса.

Подробно:

from flask import Flask, Blueprint, request, g, current_app, render_template

app = Flask(__name__)

# Blueprint — модуль приложения
bp = Blueprint("blog", __name__, url_prefix="/blog")

@bp.route("/<int:post_id>")
def post(post_id):
    return render_template("post.html", post_id=post_id)   # Jinja2

app.register_blueprint(bp)

@app.before_request
def load_user():
    g.user = get_user_from_session()   # g — per-request хранилище

Контексты Flask:

  • Application contextcurrent_app, g. Активен на время обработки запроса (или вручную через with app.app_context()).
  • Request contextrequest, session. Доступ к данным текущего запроса.

Это «магические» прокси-объекты (через werkzeug.local), привязанные к текущему контексту (раньше — потоку, теперь — contextvars).

Расширения: Flask-SQLAlchemy (ORM), Flask-Migrate, Flask-Login, Flask-RESTful, Marshmallow (сериализация).

⚠️ Ловушка: Обращение к request/current_app вне контекста запроса даёт RuntimeError: Working outside of application/request context — частая проблема в фоновых задачах и скриптах. Решение: обернуть в with app.app_context(): или app.test_request_context().

30

Как обрабатывать исключения во фреймворках?

Короткий ответ: Каждый фреймворк даёт способ перехватить исключение и вернуть структурированный ответ: FastAPI — @app.exception_handler + HTTPException; DRF — кастомный EXCEPTION_HANDLER/APIException; Flask — @app.errorhandler; Django — process_exception middleware и кастомные handler404/500.

Подробно:

# FastAPI
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.get("/items/{id}")
async def get(id: int):
    if id == 0:
        raise HTTPException(status_code=404, detail="Not found")

class TooManyError(Exception): ...

@app.exception_handler(TooManyError)            # глобальный обработчик типа
async def handler(request: Request, exc: TooManyError):
    return JSONResponse(status_code=429, content={"error": "slow down"})
# Flask
@app.errorhandler(404)
def not_found(e):
    return {"error": "not found"}, 404

# DRF — settings.py: "EXCEPTION_HANDLER": "app.views.custom_exception_handler"
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
    resp = exception_handler(exc, context)     # стандартный сначала
    if resp is not None:
        resp.data = {"error": resp.data}
    return resp

⚠️ Ловушка: Не возвращайте сырые трейсбеки клиенту в production (утечка внутренней структуры) — это особенно опасно при Django DEBUG=True. Логируйте детали на сервере, отдавайте клиенту обобщённое сообщение и request_id.

31

Как настроить CORS в Django/DRF, FastAPI и Flask?

Короткий ответ: CORS (Cross-Origin Resource Sharing) — механизм, разрешающий браузеру обращаться к API с другого origin. Настраивается middleware/расширением со списком разрешённых origins, методов и заголовков. Django — django-cors-headers, FastAPI — CORSMiddleware, Flask — flask-cors.

Подробно:

# FastAPI (Starlette)
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
    allow_credentials=True,
)
# Django — pip install django-cors-headers
MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware", ...]  # как можно выше
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_CREDENTIALS = True

# Flask — pip install flask-cors
from flask_cors import CORS
CORS(app, origins=["https://app.example.com"])

CORS — это проверка на стороне браузера, не сервера. Сервер просто отдаёт заголовки Access-Control-Allow-*, а enforcement делает браузер.

⚠️ Ловушка: allow_origins=["*"] ВМЕСТЕ с allow_credentials=True запрещено спецификацией — браузер отвергнет. Нужно перечислять конкретные origins. И allow_origins=["*"] в production — дыра: любой сайт сможет дёргать ваше API от имени пользователя (при cookie-auth особенно опасно).

32

Как реализуют аутентификацию в DRF и FastAPI?

Короткий ответ: DRF использует authentication classes (Session, Token, JWT через сторонние пакеты), заполняющие request.user. FastAPI использует security-зависимости (OAuth2PasswordBearer, HTTPBearer, APIKeyHeader) через Depends, которые валидируют токен и возвращают пользователя.

Подробно:

# DRF — JWT через djangorestframework-simplejwt
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}
# request.user заполняется автоматически из токена
# FastAPI — OAuth2 + JWT
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
import jwt

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET, algorithms=["HS256"])
    except jwt.PyJWTError:
        raise HTTPException(status_code=401)
    return get_user(payload["sub"])

@app.get("/me")
async def me(user = Depends(get_current_user)):
    return user

Различие в философии: DRF — конфигурация классов (декларативно в settings), FastAPI — зависимости (явно в сигнатуре, видно в OpenAPI).

⚠️ Ловушка: Не путайте authentication (кто ты) и authorization (что тебе можно). Также при JWT помните: токены нельзя «отозвать» без дополнительной инфраструктуры (blacklist/короткий TTL + refresh) — это не сессии. Храните SECRET вне кода и всегда указывайте algorithms=[...] явно (иначе уязвимость alg=none).

33

Как тестируют Django и FastAPI приложения?

Короткий ответ: Django — django.test.TestCase (оборачивает каждый тест в транзакцию с откатом) + Client для запросов, либо pytest-django с фикстурами. FastAPI — TestClient (на базе httpx/Starlette) либо async httpx.AsyncClient. Все используют тестовую БД.

Подробно:

# Django + pytest-django
import pytest

@pytest.mark.django_db
def test_article_list(client):
    Article.objects.create(title="X")
    resp = client.get("/articles/")
    assert resp.status_code == 200

# Django TestCase
from django.test import TestCase

class ArticleTests(TestCase):
    def setUp(self):
        Article.objects.create(title="X")   # откатится после теста
    def test_list(self):
        resp = self.client.get("/articles/")
        self.assertEqual(resp.status_code, 200)
# FastAPI
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create():
    resp = client.post("/items/", json={"name": "Book", "price": 10})
    assert resp.status_code == 200
    assert resp.json()["name"] == "Book"

# подмена зависимостей в тестах
app.dependency_overrides[get_db] = override_get_db

⚠️ Ловушка: TestCase оборачивает тест в транзакцию и откатывает — поэтому код, проверяющий поведение коммитов или transaction.on_commit хуков, не сработает; для этого используйте TransactionTestCase. В FastAPI TestClient синхронный (запускает event loop внутри) — для тестирования настоящей конкурентности нужен httpx.AsyncClient с ASGITransport.

34

Как обрабатываются статика, медиа и шаблоны?

Короткий ответ: Static — CSS/JS/изображения приложения (STATIC_URL, collectstatic). Media — загруженные пользователями файлы (MEDIA_URL/MEDIA_ROOT). Шаблоны: Django Templates (свой язык) или Jinja2 (Flask/FastAPI). В production статику отдаёт nginx/CDN, не Python.

Подробно:

# Django settings
STATIC_URL = "/static/"
STATIC_ROOT = "/var/www/static/"    # сюда collectstatic собирает всё
MEDIA_URL = "/media/"
MEDIA_ROOT = "/var/www/media/"      # загрузки пользователей
# python manage.py collectstatic  -> собирает статику со всех apps

Шаблонизаторы:

{# Django Template #}
{% for a in articles %}
  <li>{{ a.title|upper }}</li>      {# фильтры через | #}
{% endfor %}
{% url 'detail' pk=a.pk %}
{# Jinja2 (Flask / FastAPI) — больше похож на Python #}
{% for a in articles %}
  <li>{{ a.title | upper }}</li>
{% endfor %}
{{ url_for('detail', pk=a.pk) }}

Django Templates намеренно ограничены (нельзя произвольный Python в шаблоне); Jinja2 мощнее (выражения, вызовы) и быстрее.

⚠️ Ловушка: Не отдавайте статику/медиа через Django в production (DEBUG=True обслуживает их dev-сервером, но это медленно и небезопасно). Используйте nginx/CDN или WhiteNoise для статики. И медиа (пользовательские файлы) нельзя смешивать со static — их нельзя доверять, нужна проверка типа/размера.

35

Как деплоят Python веб-приложения и сколько воркеров ставить?

Короткий ответ: Стандарт: nginx (reverse proxy, TLS, статика) → gunicorn/uvicorn (app-сервер с N воркерами) → приложение. Для WSGI/CPU-bound число воркеров ≈ 2 * CPU + 1. Для async (uvicorn) — обычно по числу ядер, конкурентность даёт event loop.

Подробно:

# nginx — фронт: TLS, статика, проксирование
server {
    listen 443 ssl;
    location /static/ { alias /var/www/static/; }    # статику отдаёт nginx
    location / {
        proxy_pass http://127.0.0.1:8000;            # в gunicorn/uvicorn
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}
# WSGI (Django/Flask): 4 ядра -> 2*4+1 = 9 воркеров
gunicorn myproject.wsgi:application --workers 9 --bind 127.0.0.1:8000

# ASGI (FastAPI): обычно ~= число ядер
gunicorn main:app -k uvicorn.workers.UvicornWorker --workers 4

Формула 2 * CPU + 1: пока один воркер ждёт I/O, другой может работать на том же ядре. Это эвристика для sync-воркеров. Для async-воркеров много процессов не нужно (один воркер уже конкурентен) — берут ~число ядер для утилизации всех CPU.

Зачем nginx перед app-сервером: TLS-терминация, буферизация медленных клиентов (защита от slowloris), отдача статики, балансировка, сжатие, rate-limit.

⚠️ Ловушка: Слишком много воркеров = переподписка по памяти (каждый воркер держит свою копию приложения, БД-пул, кэш) и деградация. Учитывайте RAM, а не только CPU. И 2*CPU+1 — это про процессы; не путайте с потоками и не применяйте слепо к async-воркерам.

36

Как интегрируют фоновые задачи через Celery?

Короткий ответ: Celery — распределённая очередь задач: приложение кладёт задачу в брокер (Redis/RabbitMQ), отдельные воркеры её забирают и выполняют асинхронно. Используется для долгих/тяжёлых операций (отправка писем, генерация отчётов, обработка изображений), чтобы не блокировать веб-запрос.

Подробно:

# celery_app.py
from celery import Celery

celery = Celery("myapp", broker="redis://localhost:6379/0",
                backend="redis://localhost:6379/1")

@celery.task(bind=True, max_retries=3)
def send_report(self, user_id):
    try:
        generate_and_email(user_id)
    except SMTPError as exc:
        raise self.retry(exc=exc, countdown=60)   # ретрай через 60 сек

# в view (Django/FastAPI)
@app.post("/reports/")
def create_report(user_id: int):
    send_report.delay(user_id)        # ставит в очередь, возвращает сразу
    return {"status": "queued"}
# запуск воркера и периодических задач
celery -A myapp worker --loglevel=info --concurrency=4
celery -A myapp beat            # планировщик (cron-like задачи)

Компоненты: брокер (транспорт задач), воркеры (исполнители), backend (хранение результатов), beat (расписание).

⚠️ Ловушка: Передавайте в задачу ID, а не сами объекты (модели/сессии) — объект сериализуется и устаревает; правильно загрузить свежий из БД внутри задачи. И в Django частая ошибка — поставить задачу в Celery до коммита транзакции: воркер стартует и не находит запись. Решение — transaction.on_commit(lambda: task.delay(id)).

37

Как выбрать между Django, FastAPI и Flask?

Короткий ответ: Django — «батарейки в комплекте» для полноценных приложений с БД, админкой, auth, формами (монолит, CMS, классический backend). FastAPI — современный async API-first сервис (микросервисы, ML-инференс, высокая конкурентность I/O). Flask — микрофреймворк для маленьких/гибких приложений и прототипов.

Подробно:

Критерий Django FastAPI Flask
Тип full-stack «батарейки» API-first, async микрофреймворк
ORM свой, встроенный нет (SQLAlchemy/Tortoise) нет (расширения)
Admin да, мощный нет нет
Async частичный (с 3.x) нативный ограниченный
Валидация forms/serializers (DRF) Pydantic (встроено) вручную/расширения
Автодоки через DRF (надстройки) OpenAPI из коробки расширения
Кривая входа высокая средняя низкая
Лучшее для монолиты, CMS, CRUD микросервисы, ML API прототипы, малые сервисы

Выбор по сценарию:

  • Django (+DRF): проект с админкой, сложной доменной моделью, auth/permissions «из коробки», командой, ценящей конвенции. Классический backend и REST API поверх БД.
  • FastAPI: новый сервис с упором на API, высокая конкурентность I/O (внешние API, WebSocket), нужны автодоки и строгая валидация, ML-инференс эндпоинты.
  • Flask: небольшой сервис, прототип, нестандартная архитектура, минимум зависимостей, полный контроль над стеком.

⚠️ Ловушка: «FastAPI быстрее Django, значит берём FastAPI» — ошибочная логика. Django даёт огромную экосистему (admin, ORM, auth, формы), на воссоздание которой в FastAPI уйдёт много времени. Скорость фреймворка редко бывает бутылочным горлышком — им чаще оказывается БД. Выбирайте по задаче и экосистеме, а не по бенчмаркам «hello world».

Источники

Источники и редакционная политика

Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS