Перейти к содержанию
Публикация AiManual

Нужно ли учить Python в 2026, если AI-агент пишет пайплайны: разбор типичных ошибок

AI-агент пишет синтаксически корректный Python, и на маленьком файле всё работает. Разбираем четыре скрытые ошибки пайплайнов и три навыка, без которых их не за

Коротко

Что будет в материале

  1. 01

    Почему AI-агент не снимает необходимость понимать код

  2. 02

    Типичная ошибка №1: загрузка всего CSV в память через list()

  3. 03

    Типичная ошибка №2: потеря исключения в try/except без raise

  4. 04

    Типичная ошибка №3: неидемпотентная вставка в PostgreSQL

Да, учить Python стоит, если вы разрабатываете и поддерживаете пайплайны. AI-агент выдаст синтаксически корректный код, но без понимания языка трудно заметить скрытые дефекты: загрузку всего CSV в память, потерю исключения в try/except, неидемпотентную вставку в PostgreSQL и перезапись файла без атомарной публикации.

Точный синтаксис можно подсмотреть: в документации, в подсказке IDE, у самого агента. Заметить, что в строчке вообще есть проблема, сложнее. Разбор на Хабре описывает типичную картину: агент генерирует загрузку заказов с функциями, аннотациями типов, логированием и даже тестами, и на небольшом файле всё работает. Этот сценарий и опасен: на тестовой выгрузке из тысячи строк код проходит, на проде с сотнями миллионов строк падает по памяти, а сбой посреди записи оставляет испорченные данные.

Оговорка о границах вывода: речь не о том, что каждому инженеру данных обязательно нужен именно Python. Речь о тех, кто разрабатывает и поддерживает Python-пайплайны. И ещё: автор кода на ревью неважен, человек и модель проходят одну проверку, а за результат отвечает тот, кто принял изменение.

Почему AI-агент не снимает необходимость понимать код

Агент отталкивается от того, что видит: небольшой пример данных, текст задачи, отсутствие ошибок при запуске. Он не знает, какой объём выгрузки придёт в проде, кто перезапустит задачу после падения и что уже лежит в таблице. Отсюда типичный результат: код читается, тесты зелёные, к продакшену не готов.

К сгенерированному коду полезно задать три вопроса. Автор разбора о Python и агентах формулирует их так:

  1. Что останется после падения посреди записи?
  2. Что произойдёт при повторном запуске?
  3. Зачем эта строчка складывает всю выгрузку в память?

Ни один из них не про синтаксис. Они про поведение системы: состояние на диске, повторяемость запуска, расход памяти. Ответить на них можно, только прочитав код и поняв, что в нём происходит. Ограничение агента здесь не в качестве генерации, а в отсутствии ответственности за последствия: часть разработчиков именно поэтому не спешит отдавать код агентам, и дело не в консерватизме.

Типичная ошибка №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-пайплайны, вопрос «нужно ли учить язык» решается не возможностями агента, а вашей ответственностью за результат. Набор из четырёх вопросов закрывает большую часть скрытых дефектов: что останется после падения посреди записи, что будет при повторном запуске, зачем код держит все данные в памяти, увидят ли зависимые задачи сбой. Ответы на них требуют понимания кода, а не знания синтаксиса.

Практический порядок работы: агенту отдавайте генерацию, рефакторинг и рутину, приёмку держите на себе. Автор кода неважен, ревью для человека и модели одно. Один тест на сбой посреди записи и один прогон пайплайна дважды подряд стоят дешевле, чем разбор дублей в отчёте через неделю.

Подписаться на канал