Да, учить Python стоит, если вы разрабатываете и поддерживаете пайплайны. AI-агент выдаст синтаксически корректный код, но без понимания языка трудно заметить скрытые дефекты: загрузку всего CSV в память, потерю исключения в try/except, неидемпотентную вставку в PostgreSQL и перезапись файла без атомарной публикации.
Точный синтаксис можно подсмотреть: в документации, в подсказке IDE, у самого агента. Заметить, что в строчке вообще есть проблема, сложнее. Разбор на Хабре описывает типичную картину: агент генерирует загрузку заказов с функциями, аннотациями типов, логированием и даже тестами, и на небольшом файле всё работает. Этот сценарий и опасен: на тестовой выгрузке из тысячи строк код проходит, на проде с сотнями миллионов строк падает по памяти, а сбой посреди записи оставляет испорченные данные.
Оговорка о границах вывода: речь не о том, что каждому инженеру данных обязательно нужен именно Python. Речь о тех, кто разрабатывает и поддерживает Python-пайплайны. И ещё: автор кода на ревью неважен, человек и модель проходят одну проверку, а за результат отвечает тот, кто принял изменение.
Почему AI-агент не снимает необходимость понимать код
Агент отталкивается от того, что видит: небольшой пример данных, текст задачи, отсутствие ошибок при запуске. Он не знает, какой объём выгрузки придёт в проде, кто перезапустит задачу после падения и что уже лежит в таблице. Отсюда типичный результат: код читается, тесты зелёные, к продакшену не готов.
К сгенерированному коду полезно задать три вопроса. Автор разбора о Python и агентах формулирует их так:
- Что останется после падения посреди записи?
- Что произойдёт при повторном запуске?
- Зачем эта строчка складывает всю выгрузку в память?
Ни один из них не про синтаксис. Они про поведение системы: состояние на диске, повторяемость запуска, расход памяти. Ответить на них можно, только прочитав код и поняв, что в нём происходит. Ограничение агента здесь не в качестве генерации, а в отсутствии ответственности за последствия: часть разработчиков именно поэтому не спешит отдавать код агентам, и дело не в консерватизме.
Типичная ошибка №1: загрузка всего CSV в память через list()
csv.DictReader читает файл построчно и выдаёт по одному словарю за раз. Обёртка list() забирает все строки сразу.
import csv
with open("orders.csv", newline="", encoding="utf-8") as src:
rows = list(csv.DictReader(src))
total = sum(int(row["amount_kopecks"]) for row in rows)
Для суммы хранить строки не нужно: генератор считает прямо в потоке.
with open("orders.csv", newline="", encoding="utf-8") as src:
total = sum(int(row["amount_kopecks"]) for row in csv.DictReader(src))
Разница проявляется только на объёме. Список словарей занимает в памяти заметно больше, чем сам файл CSV: на каждую строку появляется объект словаря плюс отдельные строки ключей и значений. Пока агент проверяет код на выгрузке в 50 строк, проблема не видна. На боевой выгрузке процесс упирается в лимит памяти контейнера, и задача падает с MemoryError или убивается OOM-killer'ом.
На ревью правильнее спрашивать не «почему здесь list()», а «зачем здесь одновременно нужны все строки». Если ответа нет, памяти нужен поток, а не список. Агент при этом обычно не знает размер данных: в его контексте только тот файл, который вы приложили.
Типичная ошибка №2: потеря исключения в try/except без raise
try:
load_orders(path)
except Exception:
logger.exception("Загрузка заказов упала")
logger.exception пишет в лог сообщение и стек вызовов, но исключение дальше не уходит. После блока except выполнение продолжается как ни в чём не бывало.
В обычной Python-задаче Airflow, если других ошибок нет, такой обработчик позволяет задаче завершиться успешно: traceback в логе есть, а статус в интерфейсе зелёный. Дальше по графу пойдут шаги, которые рассчитывают на загруженные данные, и получат прошлую версию таблицы или пустоту.
Минимальное исправление:
try:
load_orders(path)
except Exception:
logger.exception("Загрузка заказов упала")
raise
Пустой raise сохраняет исходный traceback. Второй вариант: ловить не Exception целиком, а конкретные типы, которые умеете обработать, и пропускать остальные наверх. Третий: оборачивать в свой тип ошибки через raise LoadError(...) from exc, если вызывающему коду нужен единый контракт.
Вопрос на ревью: «Что произойдёт с задачей, если здесь упадёт ошибка?» Если ответ «всё равно посчитается успех», обработчик написан зря.
Типичная ошибка №3: неидемпотентная вставка в PostgreSQL
Неидемпотентная вставка, это когда повторный запуск меняет результат. Простейший вариант, который охотно генерирует агент:
INSERT INTO orders (order_id, amount_kopecks, customer_id)
VALUES (%s, %s, %s);
Первый запуск прошёл, второй упал на середине, ретрай в Airflow повторил задачу целиком. Часть заказов уже в таблице, и без уникального ключа они попадут туда второй раз. Итог: двойные суммы в отчётах, расхождение с источником, долгий разбор инцидента.
Идемпотентная версия:
INSERT INTO orders (order_id, amount_kopecks, customer_id)
VALUES (%s, %s, %s)
ON CONFLICT (order_id) DO UPDATE
SET amount_kopecks = EXCLUDED.amount_kopecks,
customer_id = EXCLUDED.customer_id;
ON CONFLICT работает только при наличии уникального индекса или первичного ключа по указанным колонкам. Без него запрос упадёт с ошибкой, и это полезнее молчаливых дублей.
Другие рабочие схемы: писать в staging-таблицу и переносить данные в основную одним MERGE либо связкой DELETE и INSERT внутри одной транзакции; удалять партицию за нужный период и заливать её заново. Общий признак у всех один: повторный запуск приводит таблицу к тому же состоянию.
Уточнение по источникам: в разборе, на который я ссылаюсь выше, этот пример только назван, без кода. Описанные выше запросы отражают стандартное поведение PostgreSQL, а не чей-то разбор.
Вопрос на ревью: «Что произойдёт, если этот пайплайн запустить дважды?»
Типичная ошибка №4: перезапись файла без атомарной публикации
Файл отчёта перезаписывается по месту:
with open("report.csv", "w", encoding="utf-8") as out:
write_report(out)
Режим "w" обрезает файл сразу при открытии. Падение процесса посреди записи оставит отчёт усечённым, а предыдущая версия уже потеряна. Отличие от базы в том, что здесь нет транзакции: откатить запись нечем.
Атомарная публикация строится на временном файле и переименовании:
import os
import tempfile
target = "report.csv"
directory = os.path.dirname(os.path.abspath(target))
fd, tmp_path = tempfile.mkstemp(dir=directory, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="") as out:
write_report(out)
out.flush()
os.fsync(out.fileno())
os.replace(tmp_path, target)
except BaseException:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
raise
Ключевые детали. Временный файл создаётся в том же каталоге, иначе os.replace упрётся в границу файловых систем. os.fsync до переименования снижает риск потерять данные при внезапном отключении питания. При ошибке временный файл удаляется в except, иначе каталог постепенно засорится мусором. shutil.move для этой задачи подходит хуже: при переносе между файловыми системами он копирует данные и атомарности не даёт.
tempfile.mkstemp создаёт файл с правами 0600. Если отчёт читают другие процессы или пользователи, права придётся выставить явно, через os.chmod перед переименованием.
Здесь та же оговорка: подробного разбора этого примера в источнике нет, ниже описан общий приём из работы с файловыми системами.
Вопрос на ревью: «Что останется после падения посреди записи?»
Три группы навыков, которые остаются необходимыми
Чтение кода. Видеть, что делает каждая строка: где данные попадают в память целиком, где исключение проглатывается, где открывается транзакция и где она закрывается. Синтаксис при этом можно подсмотреть, читать нужно смысл.
Работа с внешними системами. Понимать контракты CSV, файловой системы, PostgreSQL и оркестратора: кодировки и разделители, права на файлы, уникальные индексы, уровни изоляции, поведение ретраев в Airflow. Большинство скрытых ошибок живёт на стыках, а не внутри чистых функций.
Проверка результата после сбоя. Уметь ответить, что осталось на диске и в таблицах после падения на середине, и подтвердить это тестом. Без такого теста пайплайн проходит демо и рассыпается на первом же инциденте.
Знать весь язык для этого не нужно. Достаточно уверенно читать код, понимать модель выполнения Python и представлять, как ведут себя внешние системы при повторном запуске.
Как тестировать последствия сбоя, а не обещания функции
Обычный тест проверяет, что функция вернула ожидаемый результат. Про ошибку на середине работы он не говорит ничего. Именно такие тесты агент пишет чаще всего: успешный сценарий, аккуратные assert'ы, зелёный прогон.
Проверять нужно последствия: сохранился ли старый файл, удалился ли временный файл, не появились ли дубликаты в таблице. Схема одна: подменить функцию записи на падающую, вызвать пайплайн и проверить состояние диска.
import pytest
def test_old_report_survives_failure(tmp_path, monkeypatch):
target = tmp_path / "report.csv"
target.write_text("old,data\n", encoding="utf-8")
def boom(*args, **kwargs):
raise RuntimeError("запись упала")
monkeypatch.setattr("pipeline.write_report", boom)
with pytest.raises(RuntimeError):
pipeline.build_report(str(target))
assert target.read_text(encoding="utf-8") == "old,data\n"
assert [p.name for p in tmp_path.iterdir()] == ["report.csv"]
Здесь проверяется не обещание «отчёт записывается», а факт: старый файл цел, лишних файлов не осталось. По той же логике строится тест для базы: запустить загрузку дважды и убедиться, что число строк не удвоилось.
В командном контексте к этому добавляется воспроизводимость: если шаги агента, промпты и артефакты нигде не сохраняются, разобрать инцидент через месяц уже не получится.
Вопрос на ревью: «Что произойдёт с данными, если этот шаг упадёт посреди записи?»
Что учить в Python, если вы работаете с AI-агентами
Список короткий. Задача не написать всё самому, а проверить то, что написал агент.
- Файлы и контекстные менеджеры:
with open, режимы, кодировки,newline=""для CSV,os.replace,pathlib. - Генераторы и ленивые вычисления: разница между списком и генератором,
yield,itertools. Отсюда понятно, почемуlist()вокругDictReaderменяет профиль памяти. - Исключения:
try/except/else/finally,raiseиraise ... from, логирование, разница между «поймал и обработал» и «поймал и спрятал». - Базы данных: SQL, транзакции, уникальные индексы,
ON CONFLICT, уровни изоляции, массовая загрузка. - Оркестрация: Airflow, статусы задач, ретраи, backfill, идемпотентность шагов.
- Тестирование: pytest, фикстуры,
monkeypatch, тесты на сбой и на повторный запуск.
Эти темы дают то, чего не даёт генерация: возможность увидеть проблему до продакшена. Где агент экономит время по-настоящему, а где создаёт иллюзию работы, разобрано в материале про AI в цикле разработки: типовые задачи ускоряются в 2,5-4 раза, а новая логика только в 1,3-1,8 раза.
Вывод: умение объяснить, почему повторный запуск не испортит данные
Если вы разрабатываете и поддерживаете Python-пайплайны, вопрос «нужно ли учить язык» решается не возможностями агента, а вашей ответственностью за результат. Набор из четырёх вопросов закрывает большую часть скрытых дефектов: что останется после падения посреди записи, что будет при повторном запуске, зачем код держит все данные в памяти, увидят ли зависимые задачи сбой. Ответы на них требуют понимания кода, а не знания синтаксиса.
Практический порядок работы: агенту отдавайте генерацию, рефакторинг и рутину, приёмку держите на себе. Автор кода неважен, ревью для человека и модели одно. Один тест на сбой посреди записи и один прогон пайплайна дважды подряд стоят дешевле, чем разбор дублей в отчёте через неделю.