Курс

Backend Developer Python

Глубокое понимание Python и его среды выполнения для backend-разработки. Курс разбирает не синтаксис, а семантику языка и поведение CPython: объектную модель и работу со ссылками, модель данных и протоколы, конкурентность на asyncio и следствия глобальной блокировки интерпретатора, прикладной веб-слой (FastAPI, Django, Flask), работу с данными через ORM и продакшн-инженерию. Цель — научиться видеть стоимость абстракций и поведение кода под нагрузкой, а не просто заставлять его работать.

14модулей92урока

Программа

Что внутри курса

  1. 01

    Язык Python

    9 уроков

    Семантика языка Python и её следствия для кода: всё — объект и работа со ссылками, изменяемость, имена и замыкания, модель данных и dunder-методы, равенство и хеширование, ленивые итераторы и генераторы, декораторы, менеджеры контекста, модель исключений и dataclass. Не синтаксис, а модель поведения кода.

    1. Объекты, ссылки и изменяемостьИмя в Python — это привязка к объекту, а не ящик со значением: присваивание не копирует данные, и несколько имён могут менять один объект. Урок разводит мутацию и переприсваивание, сравнение по значению и по identity, и разбирает ловушку изменяемого значения по умолчанию.
    2. Имена, области видимости и замыканияPython определяет область имени статически: присваивание где угодно в функции делает имя локальным для всего тела, поэтому чтение до присваивания даёт ошибку. Урок разбирает разрешение имён по LEGB, различие global и nonlocal и позднее связывание, из-за которого замыкания в цикле возвращают одно и то же значение.
    3. Модель данных и dunder-методыСинтаксис и встроенные функции Python — это протоколы: len, индексация, print, in, операторы разворачиваются в вызовы специальных методов на типе объекта. Урок показывает, как объект включается в язык через dunder-методы, чем различаются repr и str и почему специальные методы ищутся на типе, а не на экземпляре.
    4. Равенство и хешированиеМножества и словари находят элемент сначала по хешу, потом подтверждают равенством, поэтому объект обязан иметь согласованные равенство и хеш. Урок разбирает контракт равный объект — равный хеш, объясняет, почему определение равенства без хеша делает объект непригодным для set и dict, и почему ключами служат только неизменяемые объекты.
    5. Итераторы и генераторыЦикл for не перебирает элементы напрямую: он берёт у объекта итератор и вызывает его, пока не придёт сигнал конца. Урок разделяет iterable и одноразовый итератор-курсор, показывает, почему повторный проход по итератору даёт пусто, и объясняет генераторы — ленивый поток значений, который приостанавливается на каждом yield.
    6. ДекораторыДекоратор — это обычная функция, принимающая функцию и возвращающая замену: запись со знаком @ равна присваиванию f = dec(f) и выполняется один раз при определении, а не при каждом вызове. Урок разбирает обёртку-замыкание, потерю метаданных без functools.wraps и декоратор с аргументами как фабрику.
    7. Менеджеры контекста и withwith — это протокол из двух методов, а не синтаксис для файлов: вход возвращает значение для as, а выход вызывается всегда, в том числе при исключении, и сам решает, подавить ошибку или пропустить. Урок разбирает класс-менеджер, семантику подавления исключений и генераторный способ через contextmanager вокруг одного yield.
    8. Исключения: модель, иерархия и EAFPВ Python исключения — штатный способ управления (проще попробовать и поймать, чем проверять заранее), а не аварийный код возврата. Урок разбирает иерархию от BaseException и правило ловли подклассов, объясняет, почему широкий и голый except прячет ошибки и перехватывает Ctrl-C, и как цепочки сохраняют исходную причину.
    9. dataclass: генерация методов и ловушкиДекоратор dataclass — это генератор кода: по аннотациям полей он пишет конструктор, представление и сравнение, а по флагам добавляет хеш и порядок строго по контракту равенства. Урок собирает модуль воедино и показывает, почему изменяемое значение по умолчанию запрещено, а frozen лишь запрещает переприсваивание полей, но не замораживает вложенные изменяемые объекты.
  2. 02

    Среда выполнения CPython

    4 урока

    Как устроен CPython: подсчёт ссылок и сборка мусора, модель памяти, компиляция в байткод и его исполнение, глобальная блокировка интерпретатора как свойство сборки. Что стоит за поведением кода под нагрузкой.

    1. Байткод и интерпретаторCPython не исполняет исходный текст напрямую: сначала он компилирует код целиком в байткод (объект кода), а затем цикл интерпретатора — стековая виртуальная машина — исполняет эти инструкции. Урок показывает две фазы на примере SyntaxError и модуля dis, объясняет, что такое .pyc-кэш, и почему байткод — деталь реализации CPython, а не свойство языка.
    2. Подсчёт ссылок и сборка мусораCPython освобождает память в два уровня: основной механизм — подсчёт ссылок, при котором объект уничтожается сразу и детерминированно, как только исчезает последняя ссылка на него. Циклический сборщик мусора лишь дополняет его: находит и разрывает ссылочные циклы, которые подсчёт ссылок освободить не может. Урок разбирает счётчик ссылок, иммортальные объекты, поколенческий сборщик gc и ловушки __del__.
    3. Модель памяти и аллокацииВ CPython любое значение — это объект в приватной куче с заголовком (счётчик ссылок и указатель на тип), а имя — лишь ссылка на него, поэтому даже число не помещается в машинное слово. Урок показывает наблюдаемую стоимость объектов через sys.getsizeof, роль специализированного аллокатора, переиспользование неизменяемых объектов и выбор между __dict__ и __slots__ как компромисс «гибкость против памяти на экземпляр».
    4. GIL и свободнопоточная сборкаГлобальная блокировка интерпретатора (GIL) в CPython гарантирует, что в каждый момент байткод исполняет только один поток, делая объектную модель неявно потокобезопасной ценой параллелизма. Отсюда: CPU-bound код не ускоряется потоками (нужны процессы или нативные расширения), а I/O-bound ускоряется, потому что на время блокировки GIL отпускается. Урок показывает, что GIL — свойство сборки, а не языка: свободнопоточная сборка (PEP 703) снимает его с оговорками, и подводит к модулю о конкурентности.
  3. 03

    Асинхронность и конкурентность

    8 уроков

    Конкурентность в Python: цикл событий asyncio, корутины и задачи, отличие ввода-вывода от вычислений, выбор между потоками и процессами. Почему блокирующий вызов останавливает цикл событий.

    1. Конкурентность и параллелизмКонкурентность — это способ структурировать программу как задачи, продвигающиеся независимо и чередуясь (возможно и на одном ядре), а параллелизм — физически одновременное исполнение на нескольких ядрах; это разные оси. Урок задаёт рамку выбора для всего модуля: узкое место решает инструмент — ожидание (I/O-bound) снимается конкурентностью через asyncio и потоки, вычисления (CPU-bound) — параллелизмом на процессах, потому что у каждого свой GIL.
    2. Блокировка и цикл событийЦикл событий asyncio — это однопоточный планировщик, который исполняет по одной задаче за раз, а переключается между ними только в точках await. Поэтому любой блокирующий вызов (обычный time.sleep, синхронный requests, тяжёлый расчёт) удерживает единственный поток и замораживает все остальные задачи и ввод-вывод на всю свою длительность. Урок объясняет кооперативную многозадачность и почему блокирующий код выносят в поток или процесс.
    3. Корутины и awaitОбъявление async def создаёт корутинную функцию, а её вызов возвращает инертный объект корутины и не исполняет ни строки тела — работу прокручивает цикл событий. Оператор await не блокирует поток: он приостанавливает корутину и уступает управление циклу до готовности результата. Урок показывает, почему вызов без await ничего не делает и почему два await подряд выполняются последовательно, а не одновременно.
    4. Задачи и одновременностьОдновременность независимых корутин даёт не await, а задача: create_task заворачивает корутину и планирует её на цикл к скорому исполнению, поэтому несколько задач перекрывают свои ожидания на одном потоке. Урок показывает gather для запуска набора и сбора результатов, TaskGroup для структурной конкурентности с отменой соседей при ошибке, и подчёркивает, что задача — не поток и параллелизма вычислений не даёт.
    5. Отмена и таймаутыОтмена в asyncio кооперативна: task.cancel() не убивает задачу мгновенно, а возбуждает CancelledError в ближайшей точке await, давая шанс на очистку в try/finally. CancelledError наследует BaseException и обычно должен пробрасываться, иначе ломаются TaskGroup и timeout, построенные на отмене. Урок показывает, что таймаут — это отмена по расписанию (asyncio.timeout и wait_for дают TimeoutError) и почему по таймауту нельзя оборвать блокирующий код без await.
    6. Потоки для ввода-вывода и блокирующего кодаВывод «раз есть GIL, потоки бесполезны» неверен: на время блокирующего ввода-вывода GIL отпускается, поэтому потоки эффективно перекрывают ожидания и остаются правильным инструментом для синхронных библиотек. Урок показывает, как запускать блокирующий код через ThreadPoolExecutor и как из asyncio выносить его из цикла через to_thread и run_in_executor, и подчёркивает, что вычисления на чистом Python потоки не ускоряют.
    7. Процессы для вычисленийНастоящий параллелизм вычислений в CPython дают процессы: каждый процесс — отдельный интерпретатор Python со своей памятью и своим GIL, поэтому multiprocessing обходит GIL и задействует несколько ядер. Плата — память не общая: аргументы и результаты пересекают границу через сериализацию (pickle), а запуск процесса дорог. Урок показывает запуск CPU-bound работы через ProcessPoolExecutor и объясняет, почему процессы не берут для ввода-вывода.
    8. Синхронизация общего состоянияGIL не делает код потокобезопасным: он сериализует байткод, но составная операция вроде counter += 1 занимает несколько байткодов, и операционная система может переключить поток посередине — отсюда гонки и потерянные обновления. Урок-капстоун показывает, как защищать критическую секцию threading.Lock для потоков и asyncio.Lock для корутин (гонка возникает на границах await), почему у процессов такой гонки нет, и сводит выбор модели со всего модуля.
  4. 04

    Типизация

    5 уроков

    Аннотации типов и постепенная типизация: как типы делают контракты кода явными, статическая проверка и её место в веб-фреймворках. Типы как инструмент, а не декорация.

    1. Аннотации ничего не проверяютРантайм Python не проверяет аннотации типов: аннотированная функция спокойно примет «неправильный» тип и исполнится, потому что аннотации — это метаданные (доступны через __annotations__), а не исполняемые проверки. Урок показывает, что язык остаётся динамически типизированным, а аннотации существуют для внешних инструментов (mypy, IDE, линтеры); если типы проверяются в рантайме, это библиотека вроде Pydantic читает аннотации сама.
    2. Постепенная типизация и статическая проверкаТипизация в Python постепенная: код аннотируют по частям, типизированные и нетипизированные участки сосуществуют, а соответствие проверяет отдельный статический инструмент (mypy, pyright) до запуска, не входящий в рантайм. Any — это люк, совместимый с любым типом в обе стороны и отключающий проверку участка. Урок показывает, что статическая проверка ловит ошибки в коде до прода, но не добавляет проверок в рантайм, поэтому данные на границах всё равно валидируют.
    3. Словарь аннотацийАннотация описывает форму значения, а не только его класс: «голый» list ничего не говорит проверщику об элементах, а list[int] — говорит. Урок даёт рабочий словарь — параметризованные контейнеры (list[int], dict[str, int]), кортежи фиксированной и переменной формы, объединения и опциональность (int | str, X | None), вызываемые (Callable[[int], str]) и алиасы типов — и напоминает, что все они описывают структуру для проверщика и не влияют на рантайм.
    4. Дженерики и протоколыДженерик (TypeVar или синтаксис [T]) сохраняет связь между типами входа и выхода вместо стирания в Any, поэтому проверщик знает, что функция вернёт тот же тип, что приняла. Протокол задаёт структурную подтипизацию (статический duck typing): тип подходит по наличию нужных методов/атрибутов, без наследования от общего базового класса. Урок снимает ложный выбор «Any или базовый класс» и напоминает, что обе конструкции статичны и в рантайме не применяются.
    5. Типы в рантайме: валидацияЕдинственное место, где типы работают в рантайме, — библиотеки валидации: Pydantic-модель читает те же аннотации полей, что и статический проверщик, но строит проверку сама — парсит и валидирует недоверенные данные против объявленных типов, приводя где возможно и возбуждая ValidationError иначе. FastAPI валидирует запросы именно через это. Урок замыкает модуль: аннотации — метаданные для двух стражей, статической проверки кода и рантайм-валидации внешних данных на границе.
  5. 05

    FastAPI

    8 уроков

    FastAPI: асинхронный веб-фреймворк на аннотациях типов поверх ASGI. Маршрутизация, внедрение зависимостей, валидация и сериализация через модели, автогенерация схемы OpenAPI.

    1. ASGI и асинхронный вебFastAPI асинхронен не по моде, а потому что стоит на ASGI — интерфейсе между async-сервером и приложением, наследнике WSGI. WSGI был единственным синхронным вызовом запрос→ответ и не умел долгоживущие соединения и async; ASGI — единственный асинхронный вызов (scope, receive, send), который умеет. Урок показывает стек FastAPI → Starlette → ASGI-сервер uvicorn и объясняет, почему async в вебе родной: нагрузка I/O-bound, и один процесс на цикле событий держит много соединений.
    2. Маршрутизация и параметрыВ FastAPI параметры не достают из объекта запроса вручную — они выводятся из сигнатуры функции: имя, совпадающее с «{item_id}» в пути, становится path-параметром, остальные аргументы — query-параметрами. Аннотации типов превращаются в парсинг и валидацию через Pydantic (неверный ввод даёт 422), значение по умолчанию делает query-параметр опциональным, а его отсутствие — обязательным. Урок показывает и ловушку порядка маршрутов: «/users/me» объявляют раньше «/users/{user_id}», иначе последний перехватит запрос.
    3. Модели запроса и валидацияТело запроса в FastAPI не разбирают вручную как словарь: его описывают Pydantic-моделью (BaseModel) и объявляют как тип параметра. FastAPI по типу распознаёт модель как тело, читает JSON, валидирует его через Pydantic (неверные данные → 422 с деталями) и передаёт в функцию типизированный объект, а не dict. Обязательность поля задаёт значение по умолчанию, а правило источников — имя в пути → path, singular-тип → query, Pydantic-модель → тело — позволяет FastAPI разобрать все три в одной сигнатуре. Одна модель задаёт форму, валидацию и часть схемы OpenAPI.
    4. Модели ответа и сериализацияТип ответа в FastAPI — не сериализация «как есть», а контракт и фильтр. Объявив его аннотацией возврата или параметром декоратора response_model, вы говорите FastAPI валидировать возвращённое, добавить JSON Schema в OpenAPI и ограничить выход только объявленными полями. Функция может вернуть больше данных, чем уйдёт клиенту, — лишнее (например, пароль) отфильтровывается, и это работает как граница безопасности: модели входа (UserIn) и выхода (UserOut) разносят. Урок показывает, когда нужен response_model вместо аннотации и как наследование даёт и типы, и фильтрацию.
    5. Внедрение зависимостейОбщие для эндпоинтов вещи — сессию БД, текущего пользователя, проверку прав, общие параметры — в FastAPI не получают вручную в каждом хендлере, а объявляют как потребность параметром Depends(provider), передавая саму функцию без вызова. На каждый запрос FastAPI вызывает поставщика, получает результат и подставляет его в параметр хендлера. Поставщики иерархичны (дерево под-зависимостей: current_user → active_user → admin_user), переиспользуются и попадают в OpenAPI, а вариант с yield управляет жизненным циклом ресурса: setup до yield, teardown (db.close()) после ответа. Это разрешение потребностей на запрос, а не глобальный контейнер синглтонов.
    6. async и блокировка в FastAPIFastAPI работает на одном цикле событий, поэтому блокирующий вызов внутри async def-хендлера замораживает весь сервер и все запросы, а не только текущий. Но FastAPI страхует: path-операции и зависимости, объявленные обычным def, он исполняет во внешнем пуле потоков — блокирующий def цикл не стопорит. Отсюда правило выбора: await-библиотека → async def; блокирующая (большинство драйверов БД) → def; не знаешь → def. Худший случай — блокирующий вызов внутри async def; чинят переходом на def или выносом вызова через asyncio.to_thread. Пул конечен и решает I/O-bound; CPU-bound масштабируют процессами, а не async def.
    7. Обработка ошибокОшибку клиенту в FastAPI сигнализируют исключением, а не собранным вручную и протащенным через слои ответом. raise HTTPException(status_code, detail) немедленно прерывает запрос из любой глубины вызова (в том числе из утилитной функции), и FastAPI строит ответ дефолтным обработчиком. Невалидный вход обрабатывается встроенно: RequestValidationError → 422, писать это не нужно. Доменные исключения отображают в ответы глобально через @app.exception_handler — хендлеры бросают чистые доменные исключения, а маппинг «исключение → HTTP» живёт в одном месте. Сигнал ошибки и её отображение в ответ — разные слои.
    8. Генерация схемы OpenAPIКапстоун модуля: интерактивная документация и контракт API в FastAPI не пишутся отдельно, а генерируются из уже написанного кода. Типы параметров, модели запроса и ответа, зависимости, статусы и ошибки FastAPI собирает в машиночитаемую схему по стандарту OpenAPI (формы данных — через JSON Schema) на /openapi.json. Из неё рождаются Swagger UI (/docs), ReDoc (/redoc) и генерация клиентского кода; /docs — лишь один потребитель контракта. Код и документация не расходятся по построению: документация выводится из кода. Схема описывает форму, а не смысл, и её обогащают описаниями и тегами.
  6. 06

    Django

    9 уроков

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

    1. Цикл запрос–ответ в DjangoОриентирующий урок модуля: Django — это «батарейки в комплекте» и мнениевый фреймворк с фиксированным пайплайном запрос→ответ, а не минимальная библиотека, которую собирают из кусков, как FastAPI. Запрос проходит по алгоритму: корневой URLconf → urlpatterns (первое совпадение по порядку) → представление view(request, kwargs) → HttpResponse; при отсутствии совпадения — обработчик ошибок. Код организован по MTV (Model — данные, Template — как показано, View — какие данные), а роль «контроллера» играет сам фреймворк. Django исторически sync/WSGI-first; async добавлен позже (урок 9). Это карта модуля.
    2. Проект, приложения и настройкиDjango-проект двухуровневый: проект — тонкий корень сборки (модуль настроек + корневой URLconf), а функциональность живёт в приложениях — самодостаточных, переиспользуемых пакетах фич (модели, представления, URL, шаблоны). Приложение становится активным только через INSTALLED_APPS: по этому списку Django при старте строит реестр приложений (AppConfig на каждое), «просто импортировать» недостаточно. settings.py — единая точка сборки: Python-модуль переменных ВЕРХНЕГО_РЕГИСТРА, на который указывает DJANGO_SETTINGS_MODULE; дефолты переопределяются вашими, а в коде их читают через объект django.conf.settings. Реестр заполняется в три фазы (django.setup()), отсюда запрет на запросы к БД при импорте и в ready().
    3. Модели и ORMМодель Django — это одновременно определение таблицы и точка входа для запросов: класс, наследующий models.Model, где каждый атрибут-поле соответствует колонке, а Django автоматически даёт API доступа к данным (SQL писать почти не нужно). Чтение ленивое — QuerySet строится и составляется в цепочку без обращения к базе, а запрос выполняется только при вычислении (итерация, print, list). Запись — в стиле Active Record: экземпляр сам себя сохраняет через save() (INSERT для нового, UPDATE для существующего), и до вызова база не затрагивается. Тип поля задаёт колонку, виджет формы и валидацию (null — про базу, blank — про валидацию). Оптимизация запросов (N+1, select_related, индексы) — отдельный модуль 09.
    4. МиграцииМиграции Django — выводимый, версионируемый мост между моделями и схемой БД, а не скрипты, которые пишут руками, и не пронумерованные файлы, исполняемые сверху вниз. Модель — источник истины: makemigrations сравнивает модели с записанной историей миграций и генерирует новые файлы-миграции (как коммиты), а migrate применяет их к базе. Миграции коммитятся в репозиторий, создаются один раз и детерминированно воспроизводятся у коллег, на staging и prod, а между собой связаны графом зависимостей (FK в другое приложение → зависимость от его миграции); номер — лишь ориентир. Границы: makemigrations не идеален на сложных изменениях и миграциях данных; базы различаются (SQLite эмулирует), откат — применением более ранней миграции.
    5. Представления и маршрутизацияВ Django маршрутизация централизована и отделена от представлений, в отличие от decorator-роутинга FastAPI. Корневой URLconf хранит urlpatterns из path(); Django перебирает их по порядку и останавливается на первом совпадении, сопоставляя только путь — без домена, query и HTTP-метода. Контракт представления: принимает HttpRequest плюс захваченные именованные части URL, возвращает HttpResponse; path-конвертер (<int:year>) и захватывает сегмент, и типизирует его, а невалидное значение даёт 404. Метод HTTP маршрут не различает — ветвление по методу делает само представление (в CBV — dispatch, отсутствующий метод → HttpResponseNotAllowed). FBV и CBV — две формы одного контракта; CBV дают переиспользование через миксины ценой читаемости. URL описывают под именем и разрешают обратно через reverse()/{% url %}, чтобы схема жила в URLconf в одном месте (DRY); крупные схемы разбивают через include().
    6. Формы и валидацияDjango Form — декларативный Python-класс (не HTML-тег form), где поля-классы (CharField, EmailField, BooleanField) задают сразу валидацию, приведение к Python-типу и виджет (HTML-представление); поля формы маппятся на HTML-элементы, как поля модели — на колонки БД. Форма бывает unbound (без данных, для показа) и bound (с данными, Form(request.POST)); проверять на валидность можно только bound-форму. Цикл в одном представлении: GET → пустая форма; POST → bound-форма → is_valid(); True → работаем с данными и редиректим; False → форма перерисовывается с введёнными данными и ошибками. Доверенный, типизированный вход — только cleaned_data после is_valid() == True (bool/int, а не сырые строки), а не request.POST; max_length даёт и maxlength в HTML, и серверную проверку — доверять можно лишь серверной. ModelForm выводит форму из модели (на нём построена админка). Это та же идея «валидация на границе», что Pydantic в FastAPI, но форма ещё рендерит и перерисовывает себя с ошибками.
    7. Аутентификация, сессии и middlewareТри «батарейки» Django связаны одной моделью. Middleware — глобальный «луковичный» слой: запрос проходит слои из MIDDLEWARE сверху вниз к представлению в ядре, ответ — через те же слои в обратном порядке; слой может замкнуть цепочку, вернув ответ до представления. Сессия — серверное состояние, привязанное к клиенту: данные на сервере, а браузеру отдаётся cookie только с id сессии (сессия и cookie — не одно и то же). Аутентификация надстроена над сессиями и подключена через AuthenticationMiddleware, который на каждом запросе кладёт в request.user объект User (или AnonymousUser; различать через is_authenticated) — вот откуда берётся request.user. authenticate() проверяет учётные данные и возвращает User/None; login() сохраняет id пользователя в сессию; logout() её очищает; пароли хранятся хешем. Аутентификация («кто ты») ≠ авторизация («что можно»: permissions/groups, гейты login_required/LoginRequiredMixin). Порядок в MIDDLEWARE значим: auth идёт после session, потому что читает пользователя из сессии — прямое следствие луковицы.
    8. Django REST frameworkDjango из уроков 1–7 ориентирован на server-rendered HTML; для JSON/HTTP API поверх него ставят отдельный слой — Django REST framework (стороннее приложение: pip install, добавить 'rest_framework' в INSTALLED_APPS). Django сам по себе не API-фреймворк. Сердце DRF — сериализатор, двусторонний мост: наружу превращает сложные данные (модели, QuerySet) в нативные Python-типы → JSON (serializer.data); внутрь валидирует входящий JSON (is_valid() → validated_data) и сохраняет через save() (create или update). Сериализатор работает как Form/ModelForm (урок 6) и воплощает ту же «валидацию на границе», что Pydantic в FastAPI (модуль 05); validated_data — аналог cleaned_data. Serializer даёт полный контроль над полями; ModelSerializer выводит поля из модели, а список fields выбирает, что отдать в JSON (по духу как response_model). ViewSet группирует CRUD-логику в один класс, а router автоматически генерирует по нему URLconf — надстройка над CBV и URLconf; permission_classes — гейт доступа поверх аутентификации из урока 7.
    9. async в DjangoЗакрывает обещание урока 1: async в Django — ретрофит поверх исходно синхронного, WSGI-first ядра, а не async-native дизайн, как FastAPI (модуль 05). Полноценный async-стек — только под ASGI; под WSGI async-view работают, но со штрафами и без эффективных долго-живущих запросов. Представление делается асинхронным через async def (FBV — целиком, CBV — обработчики методов get/post, не __init__/as_view); выгода — сотни соединений без потоков, тот же event loop, что в модуле 03. Ключевые части Django async-unsafe (прежде всего ORM): вызов из async-контекста бросает SynchronousOnlyOperation — это защита данных, а не баг. Мост между мирами: адаптеры sync_to_async()/async_to_sync() (из asgiref) и a-префиксные методы ORM (afirst, acreate, asave, async for); транзакции пока не async. Синхронный middleware между ASGI-сервером и async-view заставляет держать поток на запрос, съедая выгоду — ASGI осмыслен только при наличии async-кода.
  7. 07

    Flask

    6 уроков

    Flask: минималистичный веб-фреймворк, дающий маршрутизацию и запрос-ответ, а остальное — из расширений. Осознанный контроль над зависимостями для небольших сервисов.

    1. Философия микрофреймворка и маршрутизацияОриентирующий урок модуля: Flask — третья модель веб-фреймворка рядом с батарейным Django (модуль 06) и async-native FastAPI (модуль 05). Он микрофреймворк: «micro» — про минимальное, но расширяемое ядро (маршрутизация + запрос/ответ) и отказ решать за вас, а не про размер приложения; ORM, формы, аутентификацию добавляют расширениями. Flask — тонкий слой поверх Werkzeug (WSGI) и Jinja (шаблоны), он синхронный и WSGI-first. Приложение — явный объект app = Flask(__name__); маршрут привязывается декоратором @app.route рядом с функцией (как в FastAPI), а не через централизованный URLconf, как в Django; функция возвращает ответ (строка → Flask оборачивает сам). Переменные части URL (<int:post_id>) с конвертерами (string/int/float/path/uuid) и матчат, и типизируют сегмент; url_for строит URL по имени функции (DRY, как reverse). Даёт карту модуля: запрос/ответ, контексты, blueprints, расширения, фабрика.
    2. Запрос и ответВход обработчика — объект request (импортируется из flask): request.method, request.form (тело POST/PUT; отсутствующий ключ → 400, безопаснее .get), request.args (query), плюс files и cookies. request импортируется как глобальный, но содержит данные своего запроса и не путается при конкуренции — механика этого (контексты) в уроке 3. Возвращаемое значение Flask сам превращает в ответ: строка → 200 + text/html; dict/list → JSON через jsonify (сериализовать руками не нужно); кортеж (body, status[, headers]) задаёт статус и заголовки; готовый response — как есть. Для тонкого контроля берут объект ответа явно через make_response (заголовки, статус, set_cookie); куки ставятся на response. Перенаправление — redirect(url_for(...)), ранняя ошибка — abort(code); кастомные страницы ошибок — @app.errorhandler(code).
    3. Контексты приложения и запросаРазгадка того, что делает Flask особенным: request, session, current_app, g — не обычные глобальные объекты, а прокси к объектам, локальным для текущего контекста (обработки запроса/воркера). Поэтому «глобальный» request свой на каждый запрос и не путается при конкуренции (та же изоляция, что в модуле 03). Контекстов два: запроса (request, session — данные уровня запроса) и приложения (current_app, g — данные уровня приложения); Flask пушит оба на время запроса и снимает после, а контекст приложения пушится и для CLI-команд. Контексты нужны, чтобы не передавать app/request через все функции и, главное, чтобы избежать циклических импортов: при фабрике приложения, blueprints и расширениях объекта app для импорта нет — используют прокси current_app. g — namespace на время контекста (кэш соединения с БД + teardown_appcontext); данные g теряются после контекста, для межзапросного хранения — session или БД. Обращение к прокси вне контекста даёт RuntimeError: Working outside of application context — следствие модели, лечится with app.app_context().
    4. BlueprintsBlueprint — не приложение и не под-приложение, а запись отложенных операций (маршрутов, обработчиков, ресурсов), которые применяются к приложению в момент app.register_blueprint(bp). @bp.route лишь записывает намерение; исполняется оно при регистрации. Имя blueprint префиксует endpoint (через точку: simple_page.show), но не меняет URL; монтирование под путь задаётся отдельно — url_prefix. URL строят через url_for("bp.view"), а внутри того же blueprint — относительным url_for(".view"). Blueprint разделяет приложение на уровне Flask (одно приложение, общий конфиг), в отличие от нескольких Flask(), которые разделяют на WSGI-уровне с раздельными конфигами; цена — blueprint нельзя разрегистрировать без пересоздания приложения. Отложенная регистрация — причина, по которой blueprint пишут без готового app (связь с контекстами), и тот же механизм — центральное средство расширений; он ложится на фабрику приложения. Blueprint может нести и ресурсы (собственные шаблоны и статику).
    5. Расширения и экосистемаКак микрофреймворк дорастает до полноценного приложения: то, чего нет в минимальном ядре (БД, формы, email, аутентификация, REST), добавляют расширения — отдельные пакеты. Это большая экосистема на PyPI с именованием Flask-Foo/Foo-Flask. Расширение — не магия, а обычный пакет, интегрирующийся по единому контракту: тянет свой конфиг из app.config и получает экземпляр приложения при инициализации. Канонический паттерн — ext = Foo(); ext.init_app(app): отложенная инициализация, где объект расширения создаётся отдельно от привязки к приложению; именно это делает его совместимым с фабрикой (урок 6), где app создаётся внутри функции и его нельзя передать при импорте (мотив current_app из урока 3). Внутри расширения регистрируют операции тем же механизмом, что blueprints (урок 4), и работают через current_app (урок 3) — отсюда «working outside of application context» при инициализации вне контекста. Замыкает контраст с батарейным Django: тот даёт ORM/формы/auth в комплекте, Flask — теми же расширениями по выбору; результат сопоставим, философия «собрать нужное» vs «всё сразу» — цена и выгода микрофреймворка.
    6. Фабрика приложения и конфигурацияЗамыкающий урок: собирает конфигурацию, расширения, blueprints и контексты в один паттерн — фабрику приложения. Конфигурацию держит app.config (подкласс словаря, доступный на старте): туда пишут значения и Flask, и расширения (тот же app.config, из которого расширения тянут конфиг), и приложение. Грузят из объекта/файла/окружения (from_object — не инстанцирует класс сам, from_pyfile, from_envvar, from_prefixed_env); dev/prod — разные конфиг-объекты на старте; SECRET_KEY подписывает session-куку (отсюда «подписанная кука» из урока 3), секреты не хранят в коде. Пока app = Flask(__name__) на уровне модуля, конфиг фиксирован при импорте и экземпляр один — ломается на тестах/окружениях. Фабрика create_app() создаёт app внутри функции: грузит конфиг, привязывает расширения через init_app, регистрирует blueprints, возвращает app. Зачем: тесты с разными настройками и несколько экземпляров в одном процессе. Цена — app недоступен при импорте, поэтому в blueprints берут current_app (урок 3), расширения создают без app и цепляют init_app (урок 5), blueprints регистрируют отложенно (урок 4): три приёма модуля оказываются следствиями одного решения. Запуск — flask --app hello run, авто-детект фабрики по имени create_app/make_app.
  8. 08

    Проектирование API

    6 уроков

    Проектирование HTTP-API: методы и коды состояния, идемпотентность, версионирование и пагинация, единый формат ошибок и валидация входных данных. Контракт как обещание сервиса клиентам.

    1. Ресурсы и методыОриентирующий урок: API — это контракт (обещание сервиса клиентам: какие ресурсы, что с ними можно, что вернётся), не зависящий от фреймворка и языка; клиент программирует против контракта, а не против реализации. Ресурс — существительное, адресуемое URL (/users/42); URL идентифицирует ресурс, а не действие; сервер отдаёт представление (обычно JSON). Действие выражает HTTP-метод с фиксированной семантикой: GET (читать), POST (создать/сделать), PUT (заменить целиком), PATCH (изменить частично), DELETE (удалить), плюс HEAD/OPTIONS; операция контракта = метод + URL. PUT vs PATCH vs POST — полная замена vs частичное изменение vs создание/несейф-операция. Общие свойства методов входят в контракт: safe (только чтение — GET/HEAD/OPTIONS; отсюда почему GET нельзя для сайд-эффектов), idempotent (повтор = тот же эффект — safe + PUT + DELETE; POST/PATCH нет — подробно в уроке 3), cacheable (GET/HEAD). Ключевой сдвиг: REST кладёт глагол в метод, а существительное в URL (DELETE /users/42), тогда как RPC-мышление кладёт глагол в URL (POST /deleteUser) и теряет предсказуемость/кэшируемость/безопасные повторы. Даёт карту модуля (статусы/ошибки, идемпотентность, версионирование, пагинация, контракты).
    2. Коды состояния и формат ошибокКод состояния — часть контракта: по классу (1xx информ., 2xx успех, 3xx редирект, 4xx ошибка клиента, 5xx ошибка сервера) исход читается без разбора тела — этим пользуются клиент, кэши, прокси, ретраеры, мониторинг. Значимые коды несут разную информацию: 200/201 Created/204 No Content; 401 (не аутентифицирован) vs 403 (аутентифицирован, но нет прав); 400 (синтаксис/framing) vs 422 (валидно, но семантически неверно — типичная валидация входа); 404, 405, 409 Conflict, 429 (rate limit); 500, 502/503(+Retry-After)/504. Ключевой анти-паттерн — «200 OK с {error}»: ломает определение исхода по коду, кэш, ретраи, мониторинг и заставляет парсить тело каждого ответа. Ошибки отдают в 4xx/5xx с единым машиночитаемым форматом — Problem Details (RFC 9457, application/problem+json): type (идентификатор типа проблемы), title, status, detail, instance + расширения (массив errors с pointer). Цель стандарта — не изобретать свой формат и не переопределять семантику кодов; detail помогает клиенту исправиться, а не отлаживать — стектрейсы/SQL/внутренние детали идут в логи, не в тело. В Python-стеке FastAPI по умолчанию отдаёт {"detail": ...} (не problem+json); главное — один формат ошибок на весь сервис.
    3. Идемпотентность и ретраиИдемпотентность из свойства-в-таблице (урок 1) превращается в инструмент дизайна: это про безопасность повтора запроса при сбое сети. Метод идемпотентен, если эффект одного запроса = эффекту нескольких идентичных; идемпотентны safe-методы + PUT и DELETE, а POST/PATCH — нет. Зачем: сеть ненадёжна, ответ может потеряться, и по «нет ответа» нельзя понять, выполнился ли запрос, — клиенты повторяют (at-least-once, модуль 03). Ретрай идемпотентного метода безопасен (повтор DELETE даёт тот же эффект, хотя ответ меняется 200→404 — идемпотентность про эффект на сервере, не про ответ). Ретрай POST плодит дубликаты (двойной заказ/платёж). Неидемпотентные операции делают отказоустойчивыми через заголовок Idempotency-Key: клиент шлёт уникальный ключ, сервер на повтор возвращает сохранённый результат, не выполняя операцию заново (конвенция индустрии, IETF-draft). Идемпотентность определяется через намерение клиента, но обеспечить её обязан сервер — объявить метод мало, реализация должна соблюдать семантику. Что ретраить: 5xx/429 (с backoff), 4xx — сперва исправить запрос, неидемпотентное — только с ключом. Дизайн: предпочитать идемпотентные методы, создание через PUT с клиентским id вместо POST.
    4. Версионирование и эволюция контрактаВерсионирование — не «поставить /v1/ и бампать на любое изменение», а дисциплина эволюции контракта: как менять API, не нарушая обещание клиентам, которые деплоятся независимо. Центральное различие — breaking vs non-breaking: аддитивные изменения (новое опциональное поле, новый эндпоинт, необязательный параметр) обратно совместимы и НЕ требуют версии, потому что корректный клиент — tolerant reader и игнорирует незнакомое (принцип open/closed для схемы); breaking (удалить/переименовать поле, сменить тип, новый обязательный параметр, сменить структуру ответа или семантику кодов/auth) требуют новой версии. Правило-детектор: сломается ли существующий клиент без изменений кода. Стратегии и trade-offs: URL-путь /v1/ — дефолт публичных API (явный, кэшируемый, дебажится curl; GitHub/Stripe/Twilio); header/media-type — чистые URL, но сложнее кэш/тест; query ?v= — анти-паттерн для постоянного; date-based (Stripe-Version) — пиннинг при масштабе, дорогой. Ломающего избегают приёмом expand-and-contract (добавить новое рядом со старым → мигрировать → убрать старое), а старое выводят управляемой деприкацией (Deprecation/Sunset (RFC 8594) + migration runway + мониторинг). Главный принцип — меньше версий: версия только для настоящих breaking changes, частоту версий задаёт частота ломающих изменений.
    5. Пагинация, фильтрация, сортировкаКоллекции нельзя отдавать целиком — их пагинируют, и способ выдачи это часть контракта. offset/limit (?page=&limit= → LIMIT/OFFSET) прост и даёт random access (номера страниц), но производительность деградирует линейно с offset (БД сканирует и отбрасывает предшествующие строки: OFFSET 100000 читает 100 001 строк ради 20) и он неконсистентен при вставках/удалениях (дубли/пропуски); хорош для малых наборов и админок. cursor — «дай то, что после этого»: opaque-токен (base64, клиент не парсит, передаёт обратно), O(limit) index seek, стабилен при вставках, но только вперёд (нет прыжка на страницу); для фидов/мобильных/больших наборов. keyset — cursor на реальных значениях индексной колонки: нужен стабильный порядок + индекс + якорь, а для равных значений композитный ключ (created_at, id) как tiebreaker; для логов/аудита/очень больших таблиц. Фильтрация и сортировка — через query-параметры (?status=&sort=-created_at), и курсор кодирует их состояние. Ответ — конверт {data, next_cursor, has_more} (успешный 2xx, НЕ анти-паттерн «200+error» из урока 2). Практики: limit+1 вместо дорогого COUNT, избегать медленного total, max page size на сервере (защита), детерминированный порядок с tiebreaker, индекс на колонке сортировки (связь с модулем 09 / query-perf).
    6. Контракты, валидация и OpenAPIЗамыкающий урок: контракт из урока 1 можно сделать машиночитаемым — OpenAPI (OAS), vendor-neutral и самый распространённый формат описания HTTP-API (методы, параметры, схемы запроса/ответа, коды; вырос из Swagger 2.0 в 2015, под Linux Foundation; сама OpenAPI-дока называет API «контрактами, которые не ломают»). Машиночитаемая спецификация бьёт прозаическую документацию (та неполна, устаревает, даёт ошибки лишь в рантайме): из одной спеки генерируются синхронные доки, boilerplate/SDK клиента и сервера на любом языке, автоматическая проверка ограничений и mock-серверы для раннего тестирования. Вторая сторона контракта — валидация входа: клиенту доверять нельзя, вход валидируют на границе (в Python — Pydantic из модулей 04–05), невалидное отдают как 422 (урок 2); ключевая мысль — схема одновременно документирует и энфорсит из единого источника. Спецификацию либо пишут раньше кода (contract-first: согласовать контракт, параллельная разработка, mock; риск дрейфа спеки от кода), либо генерируют из кода (code-first, как FastAPI из аннотаций/Pydantic: спека всегда = коду; риск «дизайна из реализации»). Синтез модуля: методы/ресурсы (1), коды/ошибки (2), идемпотентность (3), версии (4), пагинация (5) — всё это содержание контракта, выразимое в OpenAPI и проверяемое валидацией.
  9. 09

    ORM и миграции

    10 уроков

    Работа с данными через ORM в Python: сессии и транзакции, ленивая и жадная загрузка, миграции схемы. Какой SQL порождает запрос ORM и как не поймать проблему N+1.

    1. Что такое ORMОриентирующий урок: ORM отображает объектную модель на реляционную (класс↔таблица, объект↔строка, атрибут↔колонка), снимая рутину object-relational impedance mismatch. Сквозная идея модуля (из роадмапа): ORM — дырявая абстракция; он не отменяет SQL, а порождает его, и не понимать этот SQL опасно (кульминация — N+1). «ORM, чтобы не знать SQL» — ложная модель; цель модуля — всегда видеть и контролировать порождаемый SQL. SQLAlchemy 2.0 — два слоя: Core (SQL Expression Language; схемо-центричный, командный, immutable — фундамент, конструирует SQL составными объектами и выполняет в транзакции) и ORM поверх (доменно-центричный, состояние-ориентированный; персистентность автоматизирована паттерном unit of work, транслирующим изменения состояния mutable-объектов в INSERT/UPDATE/DELETE). Два паттерна ORM: Data Mapper (SQLAlchemy — доменный объект отделён от персистентности, сохраняет Session) vs Active Record (Django — объект сам себя сохраняет, save()). Реляционные основы — в shared-курсе relational-databases; здесь фокус на слое ORM поверх них. Даёт карту модуля (Core, модели, сессия, транзакции, запросы/SQL, отношения и загрузка, N+1, миграции, async).
    2. Движок, соединения и CoreСлой, реально говорящий с БД, под ORM. Engine — центральный источник соединений к базе: фабрика + connection pool; создаётся один раз на БД, конфигурируется URL (dialect — какая БД + DBAPI-драйвер + расположение) через create_engine. Пул переиспользует дорогие соединения. echo=True логирует весь порождаемый SQL — главный инструмент прозрачности модуля и прямое противоядие «дырявой абстракции» из урока 1. Connection — то, чем Core взаимодействует с БД; это открытый ресурс, живущий в контексте with. Важно: DBAPI не автокоммитит — транзакция всегда открыта, при release соединения выполняется ROLLBACK, поэтому сохранение требует commit(): стили commit as you go (явные conn.commit()) и begin once (engine.begin() — весь блок как транзакция, COMMIT при успехе / ROLLBACK при исключении, обычно предпочтительнее). execute() возвращает Result — итерабельный набор строк; Row ведёт себя как namedtuple (доступ по индексу/имени/распаковкой, .mappings() для dict-доступа). Данные передают bound-параметрами (:y + словарь) — драйвер санитизирует, это защита от SQL-инъекций; список словарей → executemany. Склейка значений в строку запрещена. В ORM Engine управляется Session, чей execute() работает как Connection.execute() и берёт Connection у Engine (фасад — подробно в уроке 4).
    3. Декларативные модели и отображениеКак в SQLAlchemy 2.0 записать «класс ↔ таблица»: Declarative Mapping задаёт сразу и объектную модель, и метаданные таблицы. Каркас: Base(DeclarativeBase) → mapped-класс как подкласс с __tablename__ → колонки- атрибуты. Колонка = Mapped[тип] + mapped_column(...); имя атрибута = имя колонки. Ключевая связь с модулем 04: тип колонки выводится из Python-типа в Mapped[] (int→INTEGER, str→VARCHAR), а nullability — из наличия Optional[]; то есть аннотации здесь не хинты для линтера, а рантайм-метаданные, реально управляющие схемой (SQLAlchemy — та самая библиотека, что читает аннотации). Точный SQL-тип задают объектом (String(30)); Python↔SQL настраивается type annotation map. mapped_column — надстройка над Core Column: принимает надмножество его аргументов (primary_key, ForeignKey, server defaults, ограничения). Каждый mapped-класс требует хотя бы один primary key (по нему ORM находит объекты — связь с Identity Map, урок 4). Имя таблицы + колонки = table metadata. Рядом живёт relationship() (связь классов — детали в уроке 7), а Base.metadata.create_all() — dev-удобство; в проде схему ведут миграциями (урок 9). Контраст с Django (модуль 06): там поля-объекты + авто-id + Active Record; в SQLAlchemy — аннотации + mapped_column + Data Mapper.
    4. Сессия: Unit of Work и Identity MapSession раскрывает два паттерна, названных в уроке 1. Она — «holding zone»: объекты внутри неё это прокси к строкам БД, локальные для транзакции сессии (поверх Connection из урока 2). Identity Map: сессия держит уникальные копии — один объект на один первичный ключ, поэтому повторный get(User, 5) возвращает тот же экземпляр (is, идентичность из модуля 01); работает благодаря обязательному PK из урока 3. Unit of Work: ORM-объекты инструментированы, правки атрибутов копятся в памяти как change events и применяются вместе на flush — потому add только помечает объект (pending), flush генерирует и выполняет SQL (INSERT/UPDATE/ DELETE) в текущей транзакции, а commit фиксирует. flush ≠ commit: flush шлёт SQL (видно в транзакции, можно откатить), commit закрепляет; по умолчанию autoflush делает flush перед каждым запросом и коммитом. После commit объекты по умолчанию expired (expire_on_commit) → следующее чтение перечитывает из БД; после закрытия сессии объект detached, обращаться к нему опасно. Состояния: transient → pending → persistent → detached.
    5. Транзакции и область сессииКак долго должна жить сессия и где ставить границы транзакции. Session работает поверх Connection (урок 2), и её время жизни определяет границы транзакции: commit фиксирует, rollback откатывает, автокоммита нет. Три стиля обрамления: commit as you go (явный commit по ходу), begin once — session.begin() как framing (успех → commit, исключение → rollback до выхода наружу), и контекстный менеджер with. sessionmaker — фабрика сессий с фиксированной конфигурацией, живёт на уровне модуля рядом с Engine (аналогия: sessionmaker для сессий — то же, что Engine для соединений); её тоже безопасно делить между потоками и функциями. Разграничение времени жизни: Engine и sessionmaker долгие (модульный/глобальный уровень), Session и Connection короткие (на операцию). Правильная область сессии — одна логическая единица работы; в вебе каноничен масштаб «сессия на запрос» (в FastAPI через Depends, модуль 05). Антипаттерн — одна глобальная долгоживущая сессия: Session не потокобезопасна (разделяемое изменяемое состояние, модуль 03), копит устаревшие объекты в Identity Map (урок 4), держит транзакцию открытой. Правило: разделяют фабрику, а не сессию.
    6. Запросы и порождаемый SQLКак строят запросы в SQLAlchemy 2.0 и как читать SQL за ними (продолжение «дырявой абстракции» из урока 1). Инструмент тот же select() → Select, что у Core в уроке 2, только теперь по mapped-классам из урока 3; исполняет его Session.execute() / Session.scalars() в транзакции сессии, результат — Result из Row. Ключевой поворот: атрибуты класса (User.name) — это SQL-выражения, а не питоновские сравнения: User.name == "spongebob" рендерится в WHERE, значение уходит связанным параметром (защита от инъекций из урока 2). Форма результата зависит от того, что передали в select(): select(User) → строки с объектами (Row из одного элемента-сущности), select(User.name, User.fullname) → строки со значениями по колонкам, можно смешивать. execute() возвращает Result из Row; для «одна сущность → сразу объекты» идиоматичен scalars() (ScalarResult, первый элемент строки). where→WHERE (несколько объединяются через AND), order_by→ORDER BY, filter_by — сахар для равенств. Запрос всегда компилируется в SQL, и его надо видеть — echo=True (урок 2) или print(stmt); иначе не контролируешь производительность (дорога к N+1, урок 8). JOIN по связям и стратегии загрузки — урок 7.
    7. Отношения и стратегии загрузкиКак ORM загружает связанные объекты — самое опасное место по производительности, закладка под N+1 (урок 8). relationship() (объявлен в модели ещё в уроке 3) обозначает связь между mapped-классами поверх внешнего ключа (ForeignKey — колонка, addresses/user — связи; back_populates делает её двунаправленной). Ключевая мысль: user.addresses — не список в памяти, а потенциальный SELECT: доступ к связанному атрибуту может сходить в БД (продолжение урока 4). Сколько запросов уйдёт, решает стратегия загрузки. lazy (по умолчанию) — отдельный SELECT при первом доступе к атрибуту; удобно, но обход списка родителей даёт запрос на каждого — N+1. Eager-стратегии грузят связанное заранее: selectinload — второй SELECT ... WHERE id IN (...) для всех загруженных родителей (2 запроса, без дублирования, обычно лучший баланс для коллекций); joinedload — JOIN в основном запросе (1 запрос, но дублирует строки родителя, хорош для many-to-one); subqueryload — подзапросный вариант (в 2.0 чаще selectinload). Стратегию задают на модели (lazy=) или, предпочтительно, на запросе через loader options (.options(selectinload(...))) — это гибче под сценарий. Выбор — trade-off «число запросов vs размер запроса», и его нельзя делать не видя SQL (echo из уроков 2/6).
    8. Проблема N+1Самый частый провал производительности ORM и каноническая иллюстрация «дырявой абстракции» из урока 1. Анатомия: обход коллекции с доступом к lazy-связи (урок 7) даёт 1 запрос на список родителей + по 1 на каждый элемент = 1+N запросов (for user in users: user.addresses → SELECT ... WHERE user_id = ? на каждого). Коварство в том, что код выглядит идеально — обычный цикл по обычному списку, SQL не виден; это прямой симптом дырявой абстракции. На локальных данных летает, в проде (большой N, БД за сетью) выстреливает. Дорого не из-за объёма данных, а из-за числа round-trip'ов: каждый запрос — раунд приложение→сеть→БД с фиксированной латентностью (перекличка с I/O-ожиданием из модуля 03). Лечится eager-загрузкой из урока 7: selectinload сворачивает N+1 в 2 запроса (родители + один IN на всех детей), joinedload — в 1 (JOIN, хорош для many-to-one). Диагностика — по видимому SQL: echo=True/логи (пачка одинаковых SELECT), счётчик запросов в тестах, трейсинг в проде (модуль 12). Не специфично для SQLAlchemy: в Django (модуль 06) те же лекарства — select_related (JOIN) и prefetch_related (отдельный запрос на всех).
    9. Миграции с AlembicПочему create_all (урок 3) не годится для прода и чем ведут схему. Ключевой факт: схема БД не подстраивается под модели автоматически, а create_all создаёт только отсутствующие таблицы и не меняет существующие (не добавит колонку, не сменит тип) — а прод-данные терять нельзя. Нужен управляемый способ эволюции схемы — миграции. Alembic (от автора SQLAlchemy) хранит каждое изменение как ревизию: Python-скрипт в versions/ с функциями upgrade() (перейти к версии) и downgrade() (откатить), связанными в цепочку через down_revision до head; отметка достигнутой версии лежит в самой БД (alembic_version), имена ревизий — partial GUID, порядок задаётся ссылками, не именами файлов. Цикл: alembic revision --autogenerate → проверить скрипт → alembic upgrade head; downgrade для отката. Главная мысль: autogenerate — черновик, не истина: сравнивает Base.metadata (урок 3) с БД, ловит добавление таблиц/колонок и nullability, но переименование видит как drop+create (потеря данных!), путает смену типа и не заполняет новые NOT NULL для существующих строк — поэтому каждую миграцию читают и правят руками (backfill, настоящий rename). env.py связывает Alembic с Engine (урок 2) и target_metadata моделей. Прямая параллель — миграции Django (модуль 06: makemigrations/ migrate); мыслить как эволюцию контракта (модуль 08): non-breaking vs breaking, expand-and-contract.
    10. Асинхронный ORMФинал модуля: чем async-режим SQLAlchemy отличается от синхронного и почему «просто добавить await» недостаточно. Веб — I/O-bound (модуль 03/05), а запрос к БД тоже I/O: синхронный ORM в async-эндпоинте заблокирует event loop. Async-режим — те же понятия из уроков 2–5 с приставкой Async и await: create_async_engine + async-драйвер в URL (postgresql+asyncpg://, урок 2), AsyncSession/async_sessionmaker, awaitable-операции (await session.execute/commit/get), async with. Под капотом тот же Engine/Session поверх event loop. Ключевое правило: неявный I/O запрещён — поэтому два «тихих» похода в БД из sync-мира ломаются: lazy-загрузка связи (урок 7, user.addresses тихо делал SELECT) и чтение expired-атрибутов после commit (урок 4). Под asyncio обычный доступ к атрибуту не может запустить запрос. Отсюда практики: eager-загрузка обязательна (selectinload в самом запросе, подтягивает в рамках await execute), сессию создают с expire_on_commit=False (иначе первое чтение после commit упадёт на скрытом I/O), точечный lazy — через awaitable_attrs (явный await). Конкурентность: AsyncSession не шарят между конкурентными задачами (модуль 03 + урок 5) — своя сессия на задачу/запрос, разделяют фабрику, не сессию; в FastAPI сессия на запрос через Depends. Async оправдан на I/O-bound нагрузке с высокой конкурентностью; для простых/CPU-bound приложений sync ORM проще — async не самоцель.
  10. 10

    Тестирование

    6 уроков

    Тестирование Python-сервиса: юнит-, интеграционные и end-to-end тесты, фикстуры и изоляция зависимостей, проверка HTTP-эндпоинтов. Тесты фиксируют поведение и защищают от регрессий.

    1. Зачем тестировать и пирамида тестовСистема координат для всего модуля: зачем тесты и какие бывают, до синтаксиса pytest. У теста две роли, и ни одна — не «процент покрытия»: исполняемая спецификация (описывает, что система должна делать, на запускаемом языке; не устаревает незаметно — при расхождении падает) и защита от регрессий (при рефакторинге/новой фиче/обновлении зависимости набор говорит «работавшее всё ещё работает», делая изменения безопасными). Правильная цель — уверенность при изменениях, не число в отчёте. Пирамида тестов: много быстрых юнитов (маленький кусок логики в изоляции, миллисекунды, точная локализация — напр. инварианты сущностей из модуля 09), меньше интеграционных (несколько частей вместе через границу: код + реальная БД/ Session из модуля 09, код + HTTP), совсем мало e2e (весь путь как у пользователя, реалистичные, но медленные/хрупкие). Форма — trade-off по трём осям: скорость (юниты мс, e2e секунды), реалистичность (растёт к вершине), хрупкость/локализация (упавший юнит точен, e2e часто flaky). Тестируют поведение и контракт (модуль 08), а не реализацию — иначе тест падает на безобидном рефакторинге и мешает. Антипаттерны: перевёрнутая пирамида (ice-cream cone), гонка за 100%, flaky-тесты. Инструмент модуля — pytest (в stdlib есть unittest).
    2. Основы pytestПишем юниты из урока 1 на pytest — де-факто стандарте с минимумом церемоний. Тест — это функция test_ в файле test_.py; pytest сам находит их по соглашению об именах (test discovery: файлы test_.py/_test.py, функции test_, классы Test) — регистрация не нужна, назовёшь иначе — не увидит. Проверки пишут обычным assert (модуль 01), без assertEqual/assertTrue из xUnit; понятную диагностику со значениями подвыражений даёт assertion rewriting — pytest переписывает assert при импорте тест-модуля, поэтому подробный вывод только для собираемых тест-модулей (для внешних хелперов включают register_assert_rewrite). Проверка исключений — контекст-менеджер pytest.raises (проходит, если возникло исключение или его подкласс), с match по тексту (re.search) и доступом к excinfo.type/.value/.traceback — пригодится для доменных исключений модуля 08. Параметризация @pytest.mark.parametrize гоняет одну функцию на наборе входов/ожиданий, каждый набор — отдельный тест: расширяешь покрытие строкой данных, а не копипастой. Маркеры (@pytest.mark.): skip/skipif/xfail и свои (регистрируются в конфиге) для выборочного запуска -m/-k — так медленные интеграционные/e2e тесты отделяют от быстрых юнитов. Грабли: в тесте assert, а не return (возврат не-None игнорируется и предупреждается); float — через pytest.approx. Аргументы-фикстуры — урок 3.
    3. Фикстуры и изоляцияРаскрывает аргументы-фикстуры, обещанные в уроке 2, и решает проблему подготовки. Почти каждому тесту нужен setup (создать объект, наполнить данные, открыть соединение); два наивных пути плохи — копипастить setup (дублирование) или сделать общий глобал (тесты делят состояние, зависят от порядка — источник flaky из урока 1). Фикстура (@pytest.fixture) — третий путь: переиспользуемая подготовка + изоляция. Тест запрашивает её, объявив одноимённый аргумент; pytest выполняет фикстуру и передаёт результат — это dependency injection, тот же приём, что Depends в FastAPI (модуль 05). Фикстуры запрашивают другие фикстуры так же (по аргументам), собирая подготовку слоями, и кэшируются в пределах одного теста (не выполняются повторно). setup/teardown — через yield: до yield setup, отданное значение получает тест, после yield teardown (выполняется даже при падении) — прямое эхо контекстных менеджеров модуля 01 и yield-зависимостей модуля 05. Scope (function/class/module/session) задаёт частоту пересоздания: по умолчанию function (максимальная изоляция), шире — быстрее, но фикстура делится между тестами (риск «испачкать» состояние); компромисс изоляция vs стоимость создания, широкий scope только для дорогих и неизменяемых ресурсов. Общие фикстуры кладут в conftest.py (видны без импортов, в той же и вложенных папках); autouse=True применяет без явного запроса (умеренно, неявность мешает чтению). Главный принцип — изоляция: своё свежее состояние на каждый тест, независимость от порядка; именно она делает падение теста точным сигналом (урок 1).
    4. Тест-дублёры и мокиЗачем и что подменять в юнит-тестах. Юнит проверяет логику в изоляции, но реальный код зависит от внешнего HTTP-API, платёжного шлюза, времени, отправки письма, медленной сети — тянуть их в юнит делает тест медленным, хрупким и недетерминированным. Подставляют подделку — тест-дублёр; «мок» не синоним «подделки вообще». Виды по цели: stub (отдаёт заранее заданные ответы — накормить код данными, проверяем результат), fake (рабочая упрощённая реализация, классика — in-memory БД/репозиторий), mock (запоминает как его вызвали и позволяет это проверить — проверяем взаимодействие), spy (оборачивает настоящий объект). Инструменты: unittest.mock — Mock/MagicMock с return_value (что вернёт вызов), side_effect (функция/исключение/ последовательность — напр. бросить TimeoutError), assert_called_once_with; patch временно подменяет объект (декоратор/контекст) с ключевым правилом «patch where it's used» — патчить имя в модуле-потребителе, а не там, где определено. У pytest есть фикстура monkeypatch — подмена атрибутов/env/словарей (setattr/setenv/ setitem) с автоматическим откатом (изоляция из урока 3 внутри инструмента). Главный принцип: дублируют границы (сеть, время, сторонние сервисы), а свой доменный код не мокают — иначе получаются тесты на реализацию (урок 1): зелёные при сломанном поведении, красные при безобидном рефакторинге. DI из модуля 05 делает подмену честной без patch-хаков; иногда правильнее не мок, а настоящая зависимость — логику вокруг БД надёжнее проверять на реальной базе в интеграционном тесте (модуль 09 → урок 5). AsyncMock — урок 6.
    5. Тестирование HTTP и базы данныхИнтеграционный уровень пирамиды (урок 1): HTTP-слой (модули 05–08) и БД (модуль 09), которые юнитом не покрыть. Разбирает два заблуждения: «эндпоинт надо тестировать через запущенный сервер и реальные запросы» и «базу надо мокнуть» — оба неверны. Фреймворки дают тест-клиент (FastAPI/Starlette TestClient, Flask app.test_client()), который прогоняет запрос прямо через ASGI/WSGI-приложение in-process, без сети и порта: быстро, детерминированно, без гонок за старт сервера; тестируется всё приложение (маршрут → валидация Pydantic → обработчик → сериализация), проверяется контракт из модуля 08 (статус, тело, формат ошибки). Зависимости эндпоинта (Depends, модуль 05) в тестах переопределяют через словарь app.dependency_overrides (ключ — оригинальная функция, значение — тестовая замена; после тестов сбрасывают = {}): внешние границы — на дублёр (урок 4), свою БД — на тестовую настоящую. БД берут настоящую, не мок: мок повторяет догадки о SQL и прячет реальные ошибки (нарушенный FK, неверный WHERE, N+1 из урока 8 модуля 09, поведение транзакции) — тест зелёный при сломанном запросе. Изоляция БД-тестов: откат транзакции на каждый тест (фикстура db_session через yield, rollback из модуля 09 — самый частый приём) или чистая схема; ту же сессию отдают приложению через override. Реализм окружения — Testcontainers (реальная СУБД в Docker на время тестов) вместо соблазна подменить на SQLite (другой диалект — тесты разойдутся с продом); trade-off: медленнее и нужен Docker, зато та же СУБД, что в проде. По пирамиде интеграционных тестов меньше, чем юнитов: логику ловят юнитами, склейку — интеграцией. В Django (модуль 06) идея та же — test client/APIClient и тестовая БД. async-клиент (httpx.AsyncClient) — урок 6.
    6. Тестирование асинхронного кодаФинал модуля: как тестировать async-код (async def-эндпоинты FastAPI из модуля 05, AsyncSession из модуля 09). Наивно кажется, что достаточно написать async def test_..., но pytest — синхронный раннер: он лишь вызовет тест-функцию, а вызов корутины (модуль 03) её не запускает — тело теста не выполнится, assert внутри не проверятся, и тест ложно выглядит зелёным/пропущенным (на деле не тестирует ничего). Нужен плагин pytest-asyncio (или anyio), который исполняет тест в event loop (модуль 03): помечаешь @pytest.mark.asyncio или включаешь авто-режим asyncio_mode=auto, и внутри доступен await; assert/raises/параметризация работают как прежде. async-фикстуры (урок 3) пишутся так же, но async def с await в setup/teardown (yield работает по-прежнему) — например для AsyncSession. Для awaitable-зависимостей вместо Mock берут AsyncMock (урок 4): его можно await, проверки assert_awaited_. HTTP async: синхронный TestClient умеет вызывать async-эндпоинты (сам крутит loop), но для полностью асинхронного теста берут httpx.AsyncClient поверх ASGITransport(app=app) (мост из урока 5); БД async — AsyncSession (модуль 09 урок 10) с async-фикстурой и dependency_overrides. Главное: async меняет только запуск теста, а не его суть — пирамида (урок 1), изоляция (урок 3), «мокать границы, не свой код» (урок 4), «БД настоящая, не мок» (урок 5) остаются; async не повод писать больше тестов или лезть в конкурентность — тестируют то же поведение, просто раннеру нужен event loop.
  11. 11

    Упаковка и окружение

    6 уроков

    Упаковка и окружение Python-проекта: виртуальные окружения, управление зависимостями и файл проекта. Воспроизводимое окружение делает сборку и деплой предсказуемыми.

    1. Зачем упаковка и окруженияПостановка проблемы и словарь для всего модуля, до инструментов. Код не самодостаточен: он исполняется в окружении — конкретный интерпретатор Python + набор установленных пакетов их версий; import берёт ровно то, что установлено рядом (модули 01–02). Классическое «works on my machine» ломается на другой машине/в CI/в проде, потому что там другое окружение (другой Python вроде 3.11 vs 3.13, другая версия библиотеки с переименованной функцией, отсутствующий пакет). Глобальная установка в системный Python рождает конфликт версий: проект A хочет django==4.2, B — django==5.1, но один Python держит только одну версию — обновил для B, сломал A. Отсюда две цели модуля: изоляция (у каждого проекта своё окружение, независимое от системного и других проектов — venv, урок 2) и воспроизводимость (окружение можно точно воссоздать где угодно — объявить зависимости в pyproject, урок 3, и зафиксировать версии, урок 4). Важная сложность: ставится не список, а граф — у каждого пакета свои транзитивные зависимости (fastapi тянет starlette, pydantic и т.д.), поэтому фиксировать нужно весь граф (lock, урок 4), а не пару строк, написанных руками. Карта модуля: venv (2) → pyproject (3) → lock (4) → uv (5) → сборка/публикация (6); контейнеры (модуль 14) доводят идею воспроизводимости до уровня ОС.
    2. Виртуальные окруженияИнструмент изоляции из урока 1 — виртуальное окружение (venv), разобранное без магии. python -m venv .venv создаёт директорию с собственным site-packages (отдельным от системного), ссылкой на интерпретатор и скриптами активации; именно свой site-packages и даёт изоляцию. «Активация» (source .venv/bin/activate или .venv\Scripts\Activate.ps1) делает одну простую вещь — добавляет папку venv в начало PATH, поэтому python/ pip начинают находить интерпретатор из .venv первым (проверяется which/where), а deactivate возвращает PATH как было; сам Python не меняется — меняется, какой вызывается по имени (перекличка с тем, где import ищет пакеты, модули 01–02). После этого pip install кладёт пакеты в site-packages окружения, системный Python не тронут — конфликт версий из урока 1 исчезает; правило «одно окружение — один проект». Активация необязательна: можно звать интерпретатор по прямому пути (.venv/bin/python -m pytest) — это важно для CI и контейнеров (модуль 14), где активации в интерактивном смысле нет. Папку .venv не коммитят (.gitignore): окружение непереносимо (бинарные ссылки, платформенные пути) и не нужно хранить — оно пересоздаётся из объявленных зависимостей (уроки 3–4) одной командой; в репозитории лежит описание окружения, а само окружение — производная, собираемая на каждой машине заново. Это и есть смысл воспроизводимости.
    3. Зависимости и pyproject.tomlГде записано, какие пакеты нужны проекту: не «в установленном venv» и не «в requirements.txt как попало», а декларативно в стандартном pyproject.toml (PEP 621) — шаг от «что оказалось установлено» к «что проект объявляет потребностью» (фундамент воспроизводимости из урока 1). Один файл понятен всем инструментам и содержит три таблицы: [project] (метаданные и зависимости), [build-system] (чем собирать — урок 6), [tool.] (конфиги инструментов вроде pytest/mypy). В [project]: name/version, requires-python (мин. версия Python), dependencies — список только прямых зависимостей (транзитивные подтянет инструмент, урок 1). Зависимость — имя со спецификатором версии: >=0.110 (не ниже), >=2.0,<3 (диапазон), ~=5.1 (совместимая), == (жёстко), без ограничения (рискованно); пределы выражают договор о совместимости — прямое эхо версионирования из модуля 08 (новая major может внести breaking change): узкие == тяжело обновлять, без пределов прилетит несовместимость, разумно ограничивать сверху несовместимые ветки. Необязательные зависимости выносят в optional-dependencies (extras вроде [dev]=pytest/mypy/ruff, [gui]); ставят pip install ".[dev]", чтобы dev/тест-инструменты (модуль 10) не тянулись в прод. Между «объявил» и «установлено» — разрешение зависимостей (resolution): инструмент из прямых + транзитивных (граф урока 1) подбирает совместимый набор конкретных версий или сообщает о конфликте (A хочет pydantic<2, B — >=2). pyproject задаёт пределы намерения; точная фиксация всего набора — lock (урок 4).
    4. Фиксация версий и воспроизводимостьГлавное заблуждение темы: раз в pyproject указаны версии, воспроизводимость (урок 1) достигнута. Нет: спецификаторы — диапазоны намерения (урок 3), а не точная фиксация. При fastapi>=0.110 сегодня resolver подберёт 0.110.1, через месяц на другой машине — 0.112.0 (тоже удовлетворяет диапазону): формально ограничение соблюдено, фактически окружения разошлись — дрейф зависимостей, снова «works on my machine» с плавающей причиной, особенно среди транзитивных зависимостей (урок 1). Настоящую воспроизводимость даёт lock-файл — снимок результата разрешения: инструмент один раз строит полный граф (прямые + транзитивные) и записывает точные версии каждого пакета плюс, как правило, хеши артефактов; установка из lock ставит ровно тот же набор везде. Отсюда две разные операции: update/lock — осознанно пере-разрешить в пределах диапазонов и обновить lock (версии меняются под контролем), и install/sync — детерминированно воссоздать из lock, не пере-разрешая (версии не меняются); правило: разработчик обновляет lock осознанно и коммитит, CI/деплой (модули 10/14) только устанавливают из lock, иначе дрейф вернётся. Хеши фиксируют не только «какая версия», но «тот ли самый файл» — защита целостности от подмены артефакта под тем же номером. Коммитить lock: приложение/сервис — да (детерминизм прода), библиотека — обычно нет (объявляет диапазоны в pyproject, чтобы уживаться с чужими зависимостями; мышление о совместимости из модуля 08). Исторически роль pinned-набора играл requirements.txt (== на все пакеты, pip freeze/pip-compile); современные инструменты (урок 5) ведут lock автоматически, стандартный формат — pylock.toml.
    5. Современный инструментарий: uvКонцепции уроков 1–4 (окружение, объявление зависимостей, разрешение, lock) инвариантны — меняется лишь инструмент, которым их выполняют. Исторически на каждую задачу была своя утилита, склеиваемая руками: venv/virtualenv (окружение, урок 2), pip (установка, урок 3), pip-tools/pip-compile (lock, урок 4), pyenv (версия Python), twine (публикация, урок 6) — пять разных команд, которые легко рассогласовать. Современный подход — единый инструмент; разбирается на примере uv (менеджер пакетов и проектов на Rust), заменяющего весь этот набор и работающего в разы-десятки раз быстрее pip (скорость критична в CI, модули 10/14). uv — другой фасад к тем же концепциям, не новая теория. Проектный цикл: uv init создаёт проект и pyproject.toml (урок 3); uv add за один шаг дописывает зависимость в pyproject, при необходимости создаёт .venv (урок 2) и обновляет lock (урок 4); uv run запускает команду прямо в окружении проекта без ручной активации (урок 2). Две операции урока 4 — это две команды: uv lock (разрешить в пределах диапазонов, записать uv.lock — update) и uv sync (привести окружение в точное соответствие lock — install, детерминированно; именно её ставят в CI/контейнере). uv python install/pin закрывает и установку самого интерпретатора (часть определения окружения из урока 1). Ландшафт: pip — базовый ручной установщик без своего lock (знать обязательно, он везде); poetry/pdm — проектные менеджеры «всё-в-одном»; uv pip — совместимый режим для ускорения существующих пайплайнов. Концепции одни, поэтому переход между инструментами — смена команд, а не переучивание. Сборка/публикация (uv build/publish) — урок 6.
    6. Сборка и публикацияФинал модуля: как отдать код, чтобы его можно было установить (pip install), а не копировать вручную — собрать проект в стандартный пакет и, если нужно, опубликовать; и сразу оговорка, что публикуют не всякий проект. Копирование исходников не делает того, что делает установка: положить файлы в site-packages (урок 2), подтянуть зависимости (урок 3), зарегистрировать консольные команды — поэтому есть стандартный формат пакета. Артефакты дистрибуции двух видов: sdist (архив исходников + метаданные, может требовать шага сборки у пользователя) и wheel (готовый к установке built-формат, ставится быстро — его обычно и берёт pip); публикуют обычно оба. Не путать distribution package (то, что устанавливают — pip install fastapi) и import package (то, что импортируют — import fastapi). Сборку делает build backend из [build-system] (урок 3: hatchling/setuptools/uv-build) командой python -m build или uv build (урок 5) → dist/.whl и dist/.tar.gz; backend читает метаданные из [project] (имя, версия, зависимости) — вот зачем аккуратно заполняли pyproject. Публикуют артефакты в индекс — по умолчанию PyPI (есть песочница TestPyPI и приватные индексы) через twine upload или uv publish; имя+версия уникальны, переопубликовать занятую версию нельзя, только выпустить новую (эхо идентичности из модуля 08). Ключевое различение (продолжение app vs library из урока 4): библиотеку упаковывают и публикуют в PyPI, объявляя диапазоны зависимостей; приложение/сервис обычно не публикуют — его дистрибутив это контейнер (модуль 14) с зафиксированным из lock окружением. Ориентир: библиотеку публикуют, приложение контейнеризуют. Версия проекта — контракт (модуль 08): bump перед релизом сообщает характер изменений (патч/minor/major), тег и changelog замыкают релиз.
  12. 12

    Наблюдаемость

    5 уроков

    Наблюдаемость Python-сервиса: логи, метрики и трассировки как три канала, структурное логирование и health-проверки. Без них поведение сервиса под нагрузкой остаётся непрозрачным.

    1. Зачем наблюдаемость и три каналаСистема координат для модуля: сервис в проде — чёрный ящик, отладчик к нему не подключить, а вопросы копятся (работает ли? какие запросы тормозят? почему упал?). Наблюдаемость (observability) — свойство понимать внутреннее состояние системы по её внешним выходным данным (логи, числа, трассы), не влезая в работающий процесс; термин из теории управления. Ключевое: её закладывают в код и инфраструктуру заранее, до инцидента, а не включают когда горит. Наблюдаемость шире мониторинга (мониторинг ⊂ наблюдаемость): дашборды и алерты отвечают на заранее известные вопросы (known-unknowns — «сколько 500-х в минуту»), наблюдаемость позволяет задать новый непредвиденный вопрос постфактум (unknown-unknowns — «почему запросы этого клиента к этому эндпоинту после релиза стали медленными»). Строится на трёх взаимодополняющих каналах: логи (что случилось — дискретные события с деталями, урок 2), метрики (сколько и как быстро — числовые агрегаты во времени: rps, доля ошибок, латентность p95, память, урок 3), трассировки (где и почему медленно — путь одного запроса через систему с временем каждого шага, урок 4). Три, а не один, потому что каналы отвечают на разные классы вопросов и не заменяют друг друга: метрика замечает проблему, трасса локализует, лог объясняет детали (пример — N+1 из модуля 09 виден трассой, а не агрегатом-метрикой). С первого дня — дисциплина: структурные логи вместо print/сырого вывода (выделенный логгер) и строгий PII-контракт (нельзя класть email, телефон, реальное имя, содержимое ответов пользователя в логи/метрики/трейсы/отчёты об ошибках — жёсткое правило проекта); детали в уроке 2.
    2. Структурное логированиеПервый канал наблюдаемости (урок 1) — как писать логи, чтобы их можно было искать/фильтровать/понимать в проде, а не только читать одну строку глазами, плюс жёсткий PII-контракт. print('user', user, 'failed') не лог: нет уровней (не отличить шум от критической ошибки), нет структуры (строка, по которой не поискать), нет управления (не выключить в проде), уходит в stdout без метаданных. Для сервиса — модуль logging: Logger (берут по __name__, не корневой), уровень, Handler (куда), Formatter (как). Уровни DEBUG/INFO/WARNING/ERROR/ CRITICAL управляют детализацией: в проде обычно INFO+, DEBUG точечно — без правки кода. Ключевой сдвиг: лог — событие с полями, а не склеенная строка; в проде эмитят в JSON (timestamp, level, message, order_id...) и как поток в stdout (Twelve-Factor), чтобы агрегатор фильтровал по полю (level=error, order_id=42), строил выборки и алерты — по слитой строке так нельзя. К событиям добавляют контекст-идентификаторы (request_id, user_id) — задел под корреляцию (урок 4), но контекст не должен содержать PII. Жёсткое правило: в логи (как и метрики, трейсы, отчёты об ошибках) нельзя класть персональные данные — email, телефон, реальное имя, содержимое ответов/сообщений пользователя, тексты AI-фидбека: логи уходят в сторонние системы (фактическая передача наружу), живут долго, доступны многим — лог с email это инцидент приватности. Приём: логировать идентификаторы, а не личные данные (user_id=123, не email). В проекте PII-контракт — инвариант, а не рекомендация; код пишет через выделенный логгер-обёртку (не print/console), тихий в проде.
    3. МетрикиВторой канал наблюдаемости (урок 1) — «сколько и как быстро». Соблазн «насчитать метрики по логам» возможен, но дорог и упускает суть: метрика — другой сигнал по природе. Лог — дискретное событие с деталями; метрика — число во времени (агрегат: rps, доля ошибок, p95). Считать из логов постфактум — тяжёлый поиск ради числа, которое дешевле измерять сразу. Метрики компактны (несколько числовых рядов вместо миллиона строк) — потому на них строят дашборды и алерты. Ловушка — кардинальность лейблов: каждая уникальная комбинация labels (endpoint/method/status) — отдельный ряд; лейбл с огромным числом значений (user_id, request_id) взрывает систему; правило — только низкокардинальные измерения, и это совпадает с PII-контрактом (урок 2). Три типа: counter (только растёт — «сколько всего», из него rate), gauge (текущее, вверх-вниз — «сколько сейчас»: соединения, очередь, память), histogram (распределение по корзинам → перцентили). Латентность мерят p95/p99, не средним: среднее прячет хвосты (99 по 10 мс + один 5 c → среднее ≈60 мс отлично, а каждый сотый ждёт 5 c; p99 покажет). Что мерить: RED (Rate/Errors/Duration — сервисы/эндпоинты) и USE (Utilization/Saturation/ Errors — ресурсы: CPU, память, пул, очередь). Метрика замечает проблему (скачок p99, рост ошибок), но не объясняет какой запрос и почему — поток метрика(алерт)→трасса(где, урок 4)→лог(детали, урок 2); скачок p99 → трасса → 90% времени в БД → N+1 из модуля 09.
    4. Трассировка и корреляцияТретий канал (урок 1) — «где и почему медленно». Метрика заметила скачок p99, но логи одного сервиса не покажут сквозной путь запроса, прошедшего через обработчик, БД и вызов соседнего сервиса. Распределённая трассировка описывает запрос деревом: span — одна операция с именем, временем старта, длительностью, parent и атрибутами (метод/маршрут/статус); trace — весь запрос (корневой span + вложенные). Дерево сразу отвечает «где 100 из 120 мс» — в вызове pricing-service. Чтобы дерево собралось через границы сервисов, trace_id (общий для всех span одного запроса) передаётся между сервисами в заголовках — context propagation (W3C traceparent): сервис B продолжает ту же трассу, span разных процессов сшиваются в один trace. Корреляция замыкает задел урока 2: тот же trace_id в span и в логах → от медленной трассы к точным логам запроса и от лога с ошибкой к трассе; метрика замечает → трасса локализует → лог объясняет. Трасса делает видимым то, что агрегат прячет: N+1 из модуля 09 как серия N последовательных span к БД (метрика «доля ошибок» его не увидит — ошибок нет), медленный внешний вызов (длинный span), ожидание (паузы: очередь, блокировка, I/O-bound из модулей 03/05). Трассировать всё дорого → семплирование: head-based (решение в начале, дёшево, упустит редкую медленную) vs tail-based (после завершения, сохранить ошибочные/медленные, полнее и дороже) — компромисс объём/полнота; детали настройки — урок 5.
    5. OpenTelemetry и health-проверкиЗавершающий урок: как реально породить три сигнала (уроки 2–4) из кода и куда отправить, плюс отдельный часто путаемый сигнал — health-проверки. Наивно взять по SDK под каждый сигнал и вендора → фрагментация и lock-in (код инструментирования завязан на вендора, разнобой API, при смене бэкенда всё переписать). OpenTelemetry (OTel) — вендоронезависимый стандарт для всех трёх сигналов сразу: код инструментируется через единый API один раз, SDK экспортирует по протоколу OTLP, принять его умеют любые бэкенды (Jaeger, Prometheus, Grafana, коммерческие) → сменить поставщика можно без переписывания кода. Различать API (инструментирование) vs SDK (реализация: семплинг/батч/экспорт) vs Collector (отдельный процесс: приём/обработка/маршрутизация) — тот же приём интерфейс/реализация. Автоинструментация даёт трассы/метрики популярных библиотек почти даром без правки бизнес-кода: веб-фреймворк (FastAPI/Flask/Django, модули 05–08) → span на запрос + RED-метрики, HTTP-клиент (requests/httpx) → span на исходящие + автопередача trace_id (урок 4), SQLAlchemy (модуль 09) → span на запрос к БД → виден N+1. Health-проверки — отдельный сигнал для оркестратора (модуль 14), не метрика: liveness («жив ли процесс» → провал = перезапуск контейнера; должна быть лёгкой и без внешних зависимостей, иначе падение БД вызовет бессмысленные каскадные перезапуски) vs readiness («готов принимать трафик» → провал = убрать из балансировки, не перезапускать; проверяет БД/кэш/миграции). Полная сборка: код инструментирован → SDK/Collector экспортируют логи/метрики/трассы по OTLP → trace_id сшивает (корреляция) → health отдаёт liveness/readiness оркестратору → PII-контракт (урок 2) держится во всех сигналах (span-атрибуты, лейблы, логи).
  13. 13

    Очереди задач

    5 уроков

    Фоновые задачи и очереди: вынос долгой работы из обработчика запроса через брокер сообщений, повторные попытки и идемпотентность. Очередь развязывает сервисы во времени и сглаживает нагрузку.

    1. Зачем фоновые задачи и очередиСистема координат модуля. Есть работа, которой не место в request-пути (письмо, перекодирование видео, тяжёлый отчёт): долгая, ненадёжная/повторяемая, результат не нужен немедленно. Долгая работа в обработчике = медленный ответ (клиент ждёт минуты, таймауты), занятые ресурсы (держит воркер веб-сервера из модулей 05–06 → под нагрузкой пул исчерпан, латентность у всех растёт), хрупкость (упал/перевыкатили процесс на середине — работа потеряна; внешний сервис недоступен — запрос падает). Она связывает время ответа клиенту со временем выполнения — их надо развязать. Решение — очередь задач: обработчик (producer) кладёт задание и сразу отвечает («принято, статус позже»), отдельный процесс (worker/consumer) достаёт и выполняет в своём темпе. Даёт: развязку во времени (producer и consumer независимы), сглаживание всплесков (очередь = буфер: 10 000 писем лягут и разгребутся посильно), изоляцию сбоев (упавшая задача не роняет давно ответивший запрос, можно повторить), отдельное масштабирование (добавляй воркеров независимо от веб-серверов). Это НЕ asyncio (модуль 03): async делает один процесс эффективным на I/O, но работа внутри процесса и жизни запроса — упал процесс, всё пропало; очередь выносит работу наружу, задание durable в брокере, переживает рестарт, повторяемо; часто сочетают (async-обработчик кладёт задачу). В фон — долгое/повторяемое/не срочное; в запросе — быстрое и нужное клиенту в ответе сейчас. Цена: инфраструктура (брокер + парк воркеров, урок 2), доставка «хотя бы раз» → идемпотентность (урок 4), асинхронный UX (результат/статус позже).
    2. Брокеры и воркерыГде на самом деле живёт очередь из урока 1: не в памяти приложения (умерла бы с процессом), а в отдельном процессе — брокере сообщений (Redis, RabbitMQ), к которому по сети подключены и producer (веб-сервис), и воркеры; это разные процессы, часто разные машины, связанные только брокером, а попавшее в брокер задание сохранено (durable). Цикл сообщения: producer публикует (publish) → сообщение ждёт в очереди → свободный воркер забирает (consume) → выполняет → подтверждает (ack). Ack — сердце надёжности: пока воркер не подтвердил, брокер считает задачу невыполненной; если воркер упал до ack, брокер отдаёт её другому — задача не теряется. Следствие: воркер мог выполнить задачу (отправить письмо), но упасть до ack → брокер переотдаёт → письмо уходит второй раз. Это доставка at-least-once: не менее одного раза, но при сбоях возможны дубли; exactly-once в распределённой системе крайне трудно, поэтому живут с at-least-once и делают задачи идемпотентными (урок 4) — связка at-least-once → дубли → идемпотентность обязательна. Брокеры: RabbitMQ — настоящий брокер (маршрутизация через обменники, надёжная доставка, ack из коробки), Redis — быстрый in-memory store, используемый как брокер, проще, часто «достаточно», но гарантии скромнее (помнить про персистентность); Kafka — про потоки событий, не очередь задач. Воркеры делят одну очередь → масштабирование добавлением воркеров/процессов (для CPU-bound — отдельные процессы, обход GIL модуля 03) + concurrency внутри воркера; длину очереди наблюдают (модуль 12). Result backend (Redis/БД) хранит результаты задач по id — роль, отдельная от брокера (доставки заданий), даже если оба физически на Redis.
    3. Задачи Celery и маршрутизацияCelery — самый распространённый в Python фреймворк очередей, прячущий ручную механику брокера (сериализовать вызов, положить в Redis, распарсить в воркере) за интерфейсом «обычная функция → фоновая задача». Celery-приложение знает брокер и result backend из конфига (уроки 1–2). Функция становится задачей декоратором @app.task (код задачи должен быть доступен и producer-у, и воркеру — общий модуль). Ключевой сдвиг: send_welcome_email(123) — обычный синхронный вызов здесь и сейчас; .delay(123) НЕ выполняет функцию, а сериализует имя+аргументы в сообщение, публикует в брокер (publish из урока 2) и сразу возвращается — выполнит воркер; полная форма .apply_async(args=[...], queue=..., ...) с опциями. Возвращается AsyncResult — хэндл к будущему результату (id в result backend): .id/.ready()/.get(). Антипаттерн: .get() прямо в обработчике блокирует — снова «долгая работа в запросе» (урок 1); правильно вернуть клиенту task_id, статус смотрит позже. Аргументы уходят как сообщение (JSON) → должны быть сериализуемы: НЕ передавать ORM-объект/блоб, а id/примитивы (user_id=123), внутри задачи заново загрузить из БД (объект не сериализуется чисто, данные могли измениться, тащит сессию/скоуп которых в воркере нет — модуль 09); в задачу идентификаторы, актуальные данные задача берёт сама. Воркер запускают отдельно (celery -A myapp worker --concurrency=4), он слушает очередь и использует тот же код задач. Маршрутизация: по умолчанию всё в одну очередь, но тяжёлые (перекодирование) забьют лёгкие (письма) → несколько очередей (queue=/task_routes) + воркеры слушают нужные (-Q) → изоляция классов задач и управление приоритетами/ресурсами.
    4. Повторы и идемпотентностьНеудобная правда урока 2: at-least-once означает «не менее одного раза» = возможно несколько раз; плюс повторы при ошибках → «задача выполнилась дважды» не авария, а штатный сценарий. Причины повторного исполнения: воркер упал после работы, но до ack (брокер переотдал); задача сама попросила retry после временной ошибки; брокер в принципе допускает повторную доставку. Вывод: нельзя писать задачу как «выполнится ровно один раз» — тот же вызов с теми же аргументами может прийти снова. Идемпотентная задача — повторное выполнение с теми же входами не создаёт дополнительного эффекта (не спишет деньги дважды, не отправит второе письмо, не создаст второй заказ). Ключевое: идемпотентность — свойство вашей задачи, не Celery и не брокера (Celery обеспечит повтор, сделать его безопасным — ваша работа); та же идея, что idempotency key в модуле 08 (HTTP). Повторять осознанно: max_retries (не бесконечно, иначе битая задача долбит вечно), экспоненциальный backoff (пауза растёт 1-2-4s, а не сразу — иначе усилит нагрузку на недоступный сервис), jitter (случайный разброс против thundering herd — чтобы тысячи задач не ломанулись одновременно). Что повторять: transient (таймаут сети, временно недоступен, блокировка БД) — retry; permanent (нет user-а, невалидные данные) — бесполезно, сразу в fail (урок 5); поэтому autoretry_for указывает на конкретные временные исключения, не «на всё». Приёмы идемпотентности: operation key (проверить, не выполнена ли операция — тот же OperationKey бэкенда/модуля 08), уникальный индекс/upsert в БД (модуль 09 — БД сама отбросит дубль), «проверь-потом-сделай» (письмо уже отправлено? заказ уже paid?), idempotency key на стороне внешнего API (провайдер отбросит дубль). Общий принцип: эффект зависит от состояния, а не от числа выполнений. Один принцип на разных уровнях (HTTP-запрос модуля 08 vs фоновая задача): в распределённой системе повтор неизбежен, операции проектируют безопасными к повтору.
    5. Периодические задачи и обработка сбоевДва добивающих сюжета, замыкающих модуль: регулярная работа по расписанию и что делать при окончательном сбое. (1) Часть работы не привязана к запросу и должна идти по расписанию (ночной отчёт, чистка старых данных, синхронизация, пересчёт агрегатов). Соблазн «воркер спит в цикле» (while True: do(); sleep) плох: единая точка отказа, плохо переживает рестарт, смешивает расписание с выполнением. Нужен планировщик — Celery beat: по расписанию (интервал или crontab-выражение) кладёт обычное task-сообщение в очередь, но сам не выполняет — забирает и выполняет воркер (уроки 2–3). Разделение ролей: beat планирует (когда), воркер выполняет (что). Воркеров можно много, но beat должен быть один: два планировщика в 03:00 положат две копии → отчёт построится дважды; отсюда single scheduler + идемпотентность (урок 4) как страховка — та же дисциплина «один активный исполнитель регулярной работы», что hosted-сервисы под advisory-lock в серверном коде. (2) Когда retry исчерпаны (max_retries) или ошибка permanent — задача окончательно сбойнула; худшее — дать ей тихо исчезнуть (работа не сделана, никто не знает). Правильно не терять: отправить в dead-letter очередь/таблицу «сбойных задач» на разбор, залогировать/заалертить (модуль 12), сохранить контекст (id, аргументы-идентификаторы, причину — без PII) для понимания и повтора. DLQ — карантин, превращающий потерю в видимое событие. Фоновую работу наблюдают отдельно (модуль 12): длина очереди и лаг (растёт = воркеры не справляются, USE saturation), доля сбоев/повторов, размер DLQ, зависшие/долгие задачи — иначе очередь чёрный ящик. Сбой становится управляемым: инженер видит причину, чинит, повторяет из DLQ (безопасно — идемпотентно). Цель модуля: фоновая работа видима и управляема — каждая единица либо выполнена, либо видимо ждёт вмешательства, ничего не пропадает бесшумно.
  14. 14

    Контейнеры и CI/CD

    5 уроков

    Контейнеры и CI/CD для Python-сервиса: упаковка в образ, переменные окружения, конвейер сборки и деплоя. Воспроизводимая сборка делает деплой предсказуемым.

    1. Зачем контейнерыСистема координат модуля. В модуле 11 воспроизводимость достигнута на уровне Python: venv изолирует, lock фиксирует точные версии зависимостей — но lock фиксирует Python-пакеты, а сервис зависит от большего: версии ОС и интерпретатора, системных C-библиотек (libpq/libssl, которые тянут пакеты для БД/крипто/картинок), env, раскладки ФС. Там и прячется остаток «работает на моей машине» — сервис может не завестись на другой машине даже с идеальным lock. Контейнер закрывает этот уровень: упаковывает приложение вместе со всем окружением (интерпретатор, системные библиотеки, зависимости, файлы) в единый переносимый образ, запускаемый одинаково на ноутбуке, в CI и в проде — буквально тот же образ, а не «должно работать одинаково». Это НЕ виртуалка: ВМ эмулирует железо и запускает отдельное ядро (тяжело, гигабайты, секунды старта); контейнер делит ядро хостовой ОС и лишь изолирует процесс средствами ядра (namespaces — своё пространство процессов/сети/ФС, cgroups — лимиты), без эмуляции железа → лёгкий (мегабайты) и быстрый (старт доли секунды). Различать образ (неизменяемый шаблон = класс из модуля 01) и контейнер (запущенный экземпляр = объект); из одного образа поднимают много копий, правка = новый образ с новым тегом (иммутабельность). Контейнер эфемерен: ФС не сохраняется при пересоздании → состояние наружу (БД/кэш/объектное хранилище, не в ФС контейнера), логи в stdout (модуль 12, Twelve-Factor), сервис stateless. Даёт: паритет dev=prod, простой деплой/откат сменой тега, масштабирование копиями, один образ под разные роли (веб-сервер и Celery-воркеры из модуля 13 — меняется только команда запуска), и артефакт для CI/CD (урок 5).
    2. Dockerfile для Python-сервисаКак собрать образ (урок 1). Dockerfile — текстовый файл с инструкциями, по которым docker build собирает образ. Наивно читать его как обычный установочный скрипт («лишь бы всё поставилось»); на деле у него слоистая природа: каждая инструкция (FROM/COPY/RUN/…) создаёт слой — неизменяемый срез ФС поверх предыдущего, образ = стопка слоёв. Главное следствие — кэш: при пересборке Docker переиспользует слой, пока его входные данные не изменились; как только на шаге что-то поменялось, этот слой и все следующие пересобираются. Отсюда правило: редко меняющееся раньше, часто меняющееся позже — иначе кэш слетает на каждую правку кода и тяжёлая установка зависимостей повторяется. Инструкции: FROM (базовый образ, python:3.12-slim), WORKDIR, COPY, RUN (выполнить при сборке), ENV (урок 4), EXPOSE (документирует порт), CMD/ENTRYPOINT (что запускать при старте контейнера). Ключевое различие: RUN — при сборке, CMD/ENTRYPOINT — при старте. База: python:3.12 полный vs slim (урезанный, разумный дефолт); alpine «самый маленький», но другая libc (musl) ломает/замедляет пакеты с C-расширениями (системные зависимости урока 1) → для сервиса slim практичнее; фиксировать версию (3.12-slim, не latest). Главный приём кэша: сначала COPY только pyproject.toml/uv.lock и установка зависимостей (uv sync --frozen — строго по lock, модуль 11), потом COPY кода — пока манифест не менялся, слой зависимостей из кэша, пересборка после правки кода занимает секунды; при обратном порядке зависимости ставятся заново каждый раз. Запуск — прод-сервером (uvicorn/gunicorn для FastAPI/Django/Flask, модули 05–07), не dev-режимом, на 0.0.0.0 (не 127.0.0.1, иначе снаружи не достучаться); из одного образа разные роли (веб — uvicorn, Celery-воркер — celery -A app worker из того же образа, модуль 13) сменой команды. .dockerignore исключает .git/.venv/__pycache__/ .env из контекста сборки — быстрее и без случайных секретов в образе.
    3. Оптимизация образа и многоступенчатая сборка«Запускается» — не единственный критерий: образ едет в прод (урок 1), и его размер и содержимое — реальные свойства. Размер важен: скорость деплоя (при каждом выкате образ выкачивается на хосты — гигабайт это минуты и медленный откат, десятки МБ — секунды), хранилище/трафик реестра (качается много раз в CI и на все ноды), поверхность атаки (всё в образе потенциально уязвимо — компиляторы, shell-утилиты, dev-инструменты = лишние CVE и инструменты для атакующего внутри). Раздувают образ: инструменты сборки (gcc, build-essential, нужны только при установке C-расширений, не в рантайме), кэши pip/uv/apt, dev-зависимости (тесты/линтеры, модуль 11), лишние файлы (.git). Проблема: поставленное в тот же образ остаётся в слоях навсегда — удаление в следующем слое не уменьшает предыдущий. Решение — многоступенчатая сборка (multi-stage): несколько FROM в одном Dockerfile; стадия builder ставит/компилирует зависимости (там компиляторы, кэши, dev-мусор — но она НЕ едет в прод), финальная стадия через COPY --from=builder берёт ТОЛЬКО результат (установленное окружение /app/.venv + код) → тонкий образ: база + рантайм-зависимости + код. Плюс безопасность: по умолчанию процесс идёт от root (пробьют приложение → root в контейнере) → создать непривилегированного пользователя (adduser + USER appuser, принцип наименьших привилегий). Прочая гигиена: минимальная база (slim из урока 2; distroless/ scratch — крайний минимум без shell, мощно но трудно отлаживать), .dockerignore, чистка кэшей apt в том же RUN, и главное — никаких секретов в слоях (пароль/токен через ENV/COPY/RUN остаётся в истории образа навсегда, даже если «перезаписан» → секреты подавать в рантайме, урок 4). Сканеры CVE (trivy) в CI (урок 5) — плюс, и маленькая база облегчает и это.
    4. Конфигурация и окружениеПротиворечие из урока 1: один неизменяемый образ едет во все среды (dev/staging/prod), но у сред разные настройки (адрес БД, ключи, уровень логирования). Если настройки в коде/образе (config.py/settings.py с прописанными значениями), для прода придётся править файл и пересобирать → в прод поедет ДРУГОЙ артефакт, чем тестировали — ломает смысл контейнера. Решение — вынести конфигурацию наружу. Принцип Twelve-Factor (фактор III): всё, что различается между средами, хранится в переменных окружения, не в коде; критерий — можно ли выложить код в open-source, не раскрыв секретов. Один образ читает разные env и ведёт себя по-разному — артефакт один, поведение снаружи. Ключевое различие: build-time (зафиксировано в образе при сборке: код, зависимости, версия Python — одинаково везде) vs runtime (подаётся при запуске: адрес БД, секреты, режим — разное в каждой среде; docker run -e, --env-file, в проде — оркестратор). Секреты (из урока 3: запечённый в слой остаётся в истории навсегда): не в код/репозиторий, не в образ/слои; инъекция в рантайме через секрет-менеджер (хранилище оркестратора, Vault, KMS); можно ротировать без пересборки. Чтение в Python: os.environ работает, но сырое чтение без типов/валидации раскидано по коду; лучше pydantic-settings (модуль 04) — один класс Settings читает env, приводит типы и валидирует на старте → fail-fast: если обязательная переменная не задана или неверного типа, приложение падает сразу на старте с понятной ошибкой, а не через час на первом запросе; конфиг становится типизированным контрактом. .env — локальное удобство разработки (не печатать -e руками, грузится dotenv/pydantic-settings), но в .gitignore и .dockerignore (уроки 2–3): не едет в репозиторий и образ; в проде секреты приходят из секрет-менеджера, не из закоммиченного .env — путать это типичный источник утечек.
    5. Конвейер CI/CDФинал модуля и курса: связать код (01–08), тесты (10), упаковку (11), наблюдаемость (12) и образ (уроки 1–4) в автоматический путь от коммита до прода. Наивно — «скрипт, заливающий код на сервер»; на деле это конвейер ворот, который сперва доказывает, что изменение безопасно, и лишь потом доставляет. CI (continuous integration) — непрерывная проверка: на каждый push/PR прогоняются линт, проверка типов (mypy, модуль 04), тесты (pytest, модуль 10), сборка образа, сканы; цель — держать основную ветку всегда в проверенном состоянии и поймать поломку до мержа, не на проде. CD (delivery/deployment) — непрерывная доставка: собранный и проверенный артефакт автоматически (или по кнопке) выкатывается в среды. Коротко: CI — «изменение корректно?», CD — «доставить проверенное». Конвейер — этапы-ворота (lint→typecheck→test→build→scan→push→deploy): провал этапа останавливает пайплайн, битое не едет дальше — в прод физически не может попасть непроверенное. Тесты (модуль 10) — главные ворота: зелёные тесты = разрешение на мерж/деплой; держат main рабочей, ловят регресс рано, дают быстрый feedback (отсюда важна пирамида — быстрые unit на каждый push). Артефакт = образ, собранный ОДИН раз (уроки 1–3): тот же тег (git-SHA/семвер) из реестра едет в staging, потом prod — что тестировали, то и работает (иммутабельность урока 1); deploy = запустить нужный тег, откат = вернуть предыдущий (быстро, т.к. образы неизменяемы). Безопасный выкат сводит прошлые модули: конфиг из env в рантайме (урок 4), health/ readiness (модуль 12 — оркестратор ждёт готовности перед трафиком), rolling (постепенная замена без простоя), откат по плохим метрикам/логам; blue-green/canary — вариации той же идеи (проверяй здоровье, держи путь к откату). Кульминация курса: одно изменение проходит Python+типы (01,04) → веб/API/async/очереди/ORM (03, 05–09,13) → тесты (10) в CI → воспроизводимая упаковка (11) в образ (1–3) → env-конфиг (4) → наблюдаемый прод (12) → доставка конвейером; backend-инженерия как связный путь…

Готовьтесь по структуре, а не вслепую

Откройте курс в приложении и закрепляйте темы в тренажёре вопросов с AI-разбором.

Backend Developer Python: подготовка к собеседованию — JobJump