Перейти к содержимому

1.4 · Jupyter-ноутбуки в VS Code

Неделя 1 · Подготовка · ⏱ ~1,5 ч · Практика

Почти все курсы, cookbook-и провайдеров (Anthropic, OpenAI), примеры LlamaIndex и LangGraph и статьи про evals публикуются как ноутбуки. Ноутбук — лучший инструмент, чтобы попробовать промпт, посмотреть на эмбеддинги, разобрать ошибки evals в таблице. Но продакшен-код в ноутбуке не живёт, и умение вовремя перенести код в модуль — тоже навык, который видно в портфолио.

В этой главе подключаем ноутбук к проекту learn-python из главы 1.3: разбираем, как ноутбук устроен и где он подводит, запускаем его на окружении uv, а потом решаем, что из ноутбука переносить в модули и как не утащить в git лишнего.

Ноутбук (.ipynb) — JSON-файл со списком ячеек: кодовых и Markdown. Код выполняется ядром (kernel) — отдельным процессом Python, который живёт, пока его не перезапустят. Все ячейки делят одно пространство имён этого процесса.

VS Code (UI) Kernel (python from .venv)
┌───────────────────────┐ run ┌───────────────────────────┐
│ [1] import httpx │ ───────▶ │ globals: httpx, df, orders│
│ [2] orders = load() │ ◀─────── │ state survives between │
│ [3] orders[:5] │ output │ cells until restart │
└───────────────────────┘ └───────────────────────────┘

Ближайшая аналогия — интерактивная консоль (REPL) Python или любого другого языка, у которой ввод разбит на сохраняемые блоки, а вывод (таблицы, графики, HTML) сохраняется рядом с кодом.

2. Главное свойство и главная ловушка — состояние

Заголовок раздела «2. Главное свойство и главная ловушка — состояние»

Ячейки можно выполнять в любом порядке и сколько угодно раз. Номер [5] слева — порядок выполнения, а не позиция. Отсюда классические баги:

  • удалили ячейку, где определялась переменная, — а она всё ещё в памяти ядра, и ноутбук «работает»; после перезапуска сломается;
  • поменяли функцию выше, но не перезапустили ячейку — внизу вызывается старая версия;
  • ячейка x = x + 1 при повторном запуске даёт новый результат.

Правило: прежде чем считать ноутбук готовым или коммитить, делаем Restart Kernel → Run All. Если сверху вниз проходит — ноутбук честный.

Чтобы ядро вообще запустилось и видело пакеты проекта, его нужно правильно выбрать — это следующий шаг.

Ядро — это конкретный интерпретатор Python. Чтобы ноутбук видел пакеты проекта, он должен работать на .venv проекта, а в этом окружении должен стоять пакет ipykernel (через него VS Code говорит с интерпретатором):

Окно терминала
cd exercises/learn-python
uv add --dev ipykernel

Дальше в VS Code: открываем .ipynb → кнопка Select Kernel справа сверху → Python Environments → .venv проекта. Если .venv не видно — сначала «Python: Select Interpreter» для рабочей папки или открываем VS Code именно в папке проекта.

Ставить пакеты через %pip install в ячейке не нужно: окружения uv по умолчанию создаются без pip, а установка мимо uv add не попадёт в pyproject.toml и lock. Новая зависимость — uv add x в терминале, затем Restart Kernel.

Ядро подключено; дальше — приёмы работы, которые понадобятся в практике:

  • Выполнить ячейку: Shift+Enter (и перейти к следующей), Ctrl+Enter (остаться).
  • Переключение типа ячейки: в командном режиме (Esc) M — Markdown, Y — код; A/B — вставить выше/ниже; DD — удалить.
  • Jupyter Variables (кнопка Variables на панели ноутбука) — список переменных ядра с типами и размерами; DataFrame (таблица библиотеки pandas) открывается в Data Viewer.
  • Отладка ячейки: Debug Cell в меню рядом с кнопкой запуска, breakpoints работают как в обычном файле.
  • Restart / Interrupt kernel — кнопки на панели. Interrupt — если ячейка зависла на сетевом запросе.
  • Interactive Window: в обычном .py можно размечать ячейки комментарием # %% и запускать их по одной. Код остаётся нормальным модулем, который диффится в git, — хороший компромисс между ноутбуком и скриптом.

Ядро Jupyter — это IPython, расширенная интерактивная оболочка Python. Оно поддерживает await на верхнем уровне ячейки, потому что ядро уже крутит свой event loop:

# cell — works in Jupyter, would be a SyntaxError in a plain .py
import httpx
async with httpx.AsyncClient() as client:
r = await client.get("https://httpbin.org/get")
r.status_code

Следствие из гл. 1.3: asyncio.run(main()) в ячейке падает с RuntimeError (цикл уже запущен) — пишем await main().

Ноутбук удобен для экспериментов, но функции, написанные прямо в ячейках, нельзя протестировать и переиспользовать. Поэтому логику держим в .py-модулях, а в ноутбуке только вызываем и смотрим результаты. Чтобы правки модуля подхватывались без перезапуска ядра:

%load_ext autoreload
%autoreload 2
from models import OrderItem # module in the same project

%-команды — «магии» IPython, в обычном Python их нет. Полезные: %time / %timeit (замер), %env (лучше не использовать для ключей — см. ниже).

Разделение «логика в модуле, эксперимент в ноутбуке» подводит к общему правилу выбора:

Ноутбук Скрипт / модуль
исследование, «а что если» код, который запускается регулярно
промпт-эксперименты, просмотр ответов модели бот, сервер, MCP-сервер
графики, таблицы результатов evals тесты, CI, cron
объяснение для читателя (туториал, отчёт) всё, что импортируют другие файлы
один раз, руками нужно ревью в git-диффе

Типичный путь в AI-проекте: эксперимент в ноутбуке → функция переезжает в модуль с тестом → ноутбук её только импортирует. Для evals (недели 6–8) ноутбук — удобная «витрина» результатов, а сам прогон — скрипт.

Остался вопрос, который возникает при первом же коммите ноутбука. .ipynb — JSON, в который сохраняются выводы ячеек и счётчики выполнения. Последствия:

  • диффы шумные: перезапуск меняет все execution_count;
  • в выводе может оказаться что угодно: распечатанные заказы с данными клиентов, ответ API с личными данными, ключ, если в ячейке был print(os.environ). Это попадёт в git;
  • картинки в выводе раздувают репозиторий.

Решения: перед коммитом — Clear All Outputs (если вывод не нужен для читателя), либо инструмент nbstripout, который чистит вывод при коммите автоматически (перед установкой его стоит проверить, как любой сторонний пакет). Секреты в ноутбуке читаются только из окружения (os.environ["DEEPSEEK_API_KEY"] после загрузки .env через load_dotenv() — глава 1.5), никогда не печатаются и не вписываются в ячейку. Папка .ipynb_checkpoints/ уже в .gitignore из гл. 1.2.

В этой книге модели в ноутбуке — DeepSeek (облако, дёшево, OpenAI-совместимый API, серверы в Китае) и локальная Ollama; Claude и OpenAI — для сравнения. Сам Jupyter от провайдера не зависит, меняется немногое (ключ и установка — в главе 1.5 · Видео Карпатого + ключи API; проверено по документации 13.11.2026):

  • Клиенты в ноутбуке — асинхронные, раз top-level await работает: AsyncOpenAI(base_url="https://api.deepseek.com", api_key=os.environ["DEEPSEEK_API_KEY"]) для DeepSeek и AsyncOpenAI(base_url="http://localhost:11434/v1", api_key="ollama") для Ollama (ключ Ollama игнорирует, но SDK требует строку). Создаём их в одной ячейке вверху и переиспользуем.
  • Вывод ноутбука — это тоже данные. Ответ локальной модели о заказах и клиентах никуда не ушёл по сети, но сохранится в .ipynb и попадёт в git, если не почистить вывод. Ноутбуки с данными клиентов — вне репозитория или в игнорируемой папке.
  • Выбор модели в ноутбуке = выбор, куда уйдут данные. В одной сессии легко по ошибке передать DataFrame с заказами и данными клиентов в клиента DeepSeek вместо локального. Такие данные держим в отдельном ноутбуке, где создан только клиент Ollama.
  • Повторный запуск ячейки у DeepSeek — копейки, у Ollama — бесплатно, но минуты ожидания на большой модели. Совет «запрос и анализ — в разных ячейках» остаётся в силе.
  • Thinking у DeepSeek включён по умолчанию: ответ содержит рассуждение в поле reasoning_content рядом с content. В ноутбуке на него удобно смотреть, но печатать его целиком в коммитимый вывод не стоит — он длинный.
Claude OpenAI DeepSeek Ollama
Async-клиент в ячейке AsyncAnthropic() AsyncOpenAI() AsyncOpenAI(base_url=...) AsyncOpenAI(base_url=...) или ollama.AsyncClient()
Что в .env ключ ключ ключ только адрес и имя модели
Повторный прогон ячейки платно платно очень дёшево бесплатно, но долго
Данные клиентов в ячейке никогда никогда никогда да, но вывод не коммитить

Для практики этой главы модели не нужны; если Ollama уже стоит — бонус ниже на ней.

  1. Открываем Jupyter Notebooks in VS Code → разделы про создание ноутбука, выбор ядра, запуск ячеек, Variables Explorer и Data Viewer, отладку (≈30 мин). Особое внимание — выбору ядра и кнопкам Restart/Interrupt.
  2. Открываем гайд uv Using uv with Jupyter → раздел про VS Code и про установку ipykernel в проект (≈10 мин). Берём оттуда, почему %pip в uv-окружении ведёт себя иначе.
  3. (По желанию) Python Interactive window → про ячейки # %% в .py (≈10 мин).
  4. Если нужен расширенный контекст про экосистему — jupyter.org (≈5 мин), только чтобы понимать разницу Notebook / JupyterLab / ядро.
  5. (≈10 мин, к бонусу) Ollama: OpenAI compatibility — какой base_url и что ключ не проверяется; DeepSeek Quick Start — base_url и пример на openai. Вывод: в ноутбуке меняется только адрес и ключ клиента.
  • Скрытое состояние. Ноутбук, который работает только при определённом порядке ячеек, — не результат. Restart + Run All перед коммитом.
  • Не то ядро. ModuleNotFoundError: No module named 'pydantic' в ноутбуке, хотя uv run работает, почти всегда значит, что выбрано системное ядро, а не .venv.
  • %pip install в ячейке ставит пакет мимо lock-файла (или не работает вовсе в uv-окружении). Только uv add + Restart Kernel.
  • Секреты и данные в выводе. Ключи и переменные окружения не печатаем; выводы с данными клиентов и заказов не коммитим. Для экспериментов с базой магазина — только копия через VACUUM INTO вне репозитория, только чтение. Данные в учебной базе выдуманы, но обращаемся с ними как с настоящими — эта привычка понадобится на работе.
  • Долгие ячейки с сетевыми вызовами. Повторный запуск ячейки со 100 запросами к LLM — это повторная оплата. Результаты кешируем в переменную или файл и разделяем «запрос» и «анализ» на разные ячейки.
  • Ноутбук как продакшен. Если код из ноутбука нужно запускать по расписанию или из бота — его место в модуле с тестами.
  • Устанавливаем расширение Jupyter в VS Code (обычно ставится вместе с Python; проверяем, что оно от Microsoft).
  • В exercises/learn-python/ выполняем uv add --dev ipykernel.
  • Создаём exercises/learn-python/explore.ipynb, выбираем ядро .venv. Первая ячейка:
    import sys
    sys.executable # must point into .../learn-python/.venv/
  • Повторяем упражнение из гл. 1.3 интерактивно:
    • ячейка с %load_ext autoreload / %autoreload 2 и импортом OrderItem из models.py;
    • ячейка, которая читает order_items.jsonl и складывает валидные позиции заказов в список, невалидные — в список ошибок;
    • Markdown-ячейка с выводами: какие ошибки нашлись;
    • ячейка с OrderItem.model_json_schema().
  • Меняем валидатор в models.py (например, своё правило для количества) и, не перезапуская ядро, перезапускаем ячейку валидации — изменение должно подхватиться благодаря autoreload.
  • Async-ячейка: from fetch_compare import parallel и await parallel() прямо в ячейке (если asyncio.run(...) в файле стоит под if __name__ == "__main__":, импорт его не запустит — гл. 1.1); затем убеждаемся, что asyncio.run(parallel()) здесь падает, и читаем ошибку.
  • Открываем Jupyter Variables и находим список позиций; ставим breakpoint и запускаем Debug Cell.
  • Устраиваем ловушку состояния: определяем fee = 0.025, используем её ниже, удаляем ячейку с определением. Ноутбук продолжит работать. Restart Kernel → Run All — видим NameError. Исправляем.
  • Создаём scratch.py с двумя ячейками # %% и запускаем их в Interactive Window.
  • (Бонус, после главы 1.5, когда Ollama установлена) Ячейка с AsyncOpenAI(base_url="http://localhost:11434/v1", api_key="ollama") и await одного вызова к локальной модели: «Объясни в одном предложении, что такое средний чек». Ниже — отдельная ячейка, которая только печатает сохранённый ответ и usage (сколько токенов ушло на запрос и ответ; токены — глава 1.5). Перезапускаем вторую ячейку несколько раз — запрос не повторяется.
  • (Для сравнения, по желанию) Тот же вопрос в DeepSeek в соседней ячейке; смотрим reasoning_content и сравниваем completion_tokens с локальной моделью. Только учебный вопрос, никаких данных магазина.
  • Перед коммитом: Restart + Run All, Clear All Outputs (или оставляем вывод, если в нём только вымышленные данные), git diff --stat глазами.
  • Ноутбук открывается в VS Code и работает на ядре из .venv проекта (sys.executable это подтверждает).
  • Упражнение из гл. 1.3 повторено в ноутбуке и проходит Restart + Run All.
  • Можем объяснить, когда писать ноутбук, а когда модуль, и почему asyncio.run в ячейке не работает.

Вопрос 1

Что такое ядро ноутбука и сколько его состояние живёт?

Отдельный процесс Python, который выполняет ячейки; его переменные живут, пока ядро не перезапущено или не закрыто, независимо от того, какие ячейки есть в файле.

Вопрос 2

Номер [7] слева от ячейки означает:

  • a) позицию ячейки в файле
  • b) порядок выполнения в текущей сессии ядра
  • c) число запусков
  • d) номер строки

b. Порядок выполнения; ячейки могли запускаться не сверху вниз.

Вопрос 3

В ноутбуке import pydantic даёт ModuleNotFoundError, а uv run python -c "import pydantic" работает. Что проверить первым?

Выбранное ядро: скорее всего, это системный Python, а не .venv проекта. Select Kernel → .venv, проверить sys.executable.

Вопрос 4

Как правильно добавить новую библиотеку для ноутбука в uv-проекте?

uv add <пакет> в терминале проекта, затем Restart Kernel. Не %pip install: установка пройдёт мимо pyproject.toml и lock (или не сработает вовсе).

Вопрос 5

Что не так в этой ячейке?

import os
print(os.environ)
resp = call_llm(os.environ["OPENAI_API_KEY"], prompt)

print(os.environ) выводит все переменные окружения, включая ключи, — они сохранятся в выводе ноутбука и могут попасть в git. Ключи никогда не печатают; SDK обычно сам читает их из окружения.

Вопрос 6

Почему в ячейке можно писать await main(), а в .py-файле на верхнем уровне нельзя?

IPython-ядро уже держит запущенный event loop и разрешает top-level await. В обычном скрипте цикла нет, его надо запустить через asyncio.run.

Вопрос 7

Что делает %autoreload 2?

Перед каждым выполнением ячейки перезагружает изменённые модули, так что правки в .py подхватываются без перезапуска ядра.

Вопрос 8

Какие два признака говорят, что код пора переносить из ноутбука в модуль?

Например: его надо запускать регулярно или из другого кода (бот, cron); его импортируют другие файлы; ему нужны тесты; нужен нормальный ревью в git-диффе.

Вопрос 9

Зачем перед коммитом делать Restart Kernel → Run All?

Чтобы убедиться, что ноутбук не зависит от скрытого состояния и порядка запусков и воспроизводим сверху вниз.

Вопрос 10

Чем ячейки # %% в .py-файле лучше .ipynb для git?

Это обычный Python: чистые диффы, нет сохранённых выводов и счётчиков, его можно импортировать, линтовать и тестировать.

Вопрос 11

Ячейка отправляет 200 запросов к LLM и сразу строит по ним таблицу. Что здесь не так с точки зрения стоимости?

Запрос и анализ в одной ячейке: каждая правка таблицы перезапускает 200 платных запросов. Результаты нужно сохранить (в переменную или файл) в отдельной ячейке и анализировать уже их.

Вопрос 12

Копия базы магазина с данными клиентов разбирается в ноутбуке, в котором вверху созданы два клиента — deepseek и local. Чем это опасно и как устроить работу безопаснее?

Одна опечатка в имени клиента — и данные клиентов уходят в облако (в случае DeepSeek — на серверы в Китае). Для таких данных — отдельный ноутбук, где создан только клиент локальной Ollama.

Вопрос 13

Локальная модель Ollama ответила по заказам из копии базы с данными клиентов; данные никуда не отправлялись. Можно ли коммитить ноутбук с этим выводом?

Нет. Вывод сохраняется в .ipynb и пересказывает данные заказов и клиентов; в git он не должен попасть. Clear All Outputs или держать такой ноутбук вне репозитория / в игнорируемой папке.

Глава 1.5 · Видео Карпатого + ключи API: интуиция о том, как устроены LLM, ключ DeepSeek и установка Ollama (ключи Anthropic и OpenAI — по желанию, для сравнения). Ноутбук из этой главы станет местом для первых вызовов модели в конце недели.