Первый Python-проект: uv, зависимости и команды
От пустой папки до проекта, который собирается одинаково на любой машине: uv init, закреплённая версия Python, lock-файл, первые зависимости и свои команды вида uv run lint.
miki/pervyy-python-proekt-uv-zavisimosti-i-komandy · v1
От пустой папки до проекта, который собирается одинаково на любой машине: uv init, закреплённая версия Python, lock-файл, первые зависимости и свои команды вида uv run lint.
Первое, что ломается в чужом проекте, — это окружение. «У меня работает» не является аргументом, если у соседа не собирается. Прежде чем написать первую строку кода, мы делаем так, чтобы проект одинаково поднимался на любой машине.
Что должно быть готово до начала: рабочее место из предыдущего списка — VS Code, git, Docker и uv установлены, папка проекта создана и открыта в редакторе.
Документация, которая пригодится: доки uv · проекты в uv · зависимости · спецификация pyproject.toml
Создание проекта
Выполняй во встроенном терминале VS Code, находясь в папке проекта. Если команда не найдена — вернись к списку про рабочее место.
uv --version- Команда напечатала версию
- Терминал открыт в папке проекта, а не где-то ещё
Точка в конце означает «в текущей папке».
Флаг --package обязателен. Без него uv создаст простой скриптовый проект без раздела [build-system], и свои команды (шаги ниже) просто не заработают — uv run lint ответит Failed to spawn: lint / program not found.
Посмотри, что появилось: pyproject.toml, README.md, .python-version, .gitignore и папка src/ с пакетом.
uv init --package .- В папке появился pyproject.toml
- В нём есть раздел [build-system] — это главный признак, что флаг сработал
- Появилась папка src с пакетом внутри
После команды открой .python-version — там одна строка с версией.
Заодно поправь в pyproject.toml строку requires-python — uv мог поставить туда более старую версию:
uv python pin 3.12- Файл .python-version содержит 3.12
- В pyproject.toml requires-python тоже >=3.12
Структура
uv создал src/<имя_проекта>/. Нам нужен пакет app/ в корне.
Перенеси папку и удали пустую src, а затем скажи сборщику, где искать код — добавь в pyproject.toml перед разделом [build-system]:
Папки scripts ещё нет — создадим её через несколько шагов, поэтому вписываем сразу обе.
- Папка app лежит в корне проекта
- Папки src больше нет
- В pyproject.toml есть раздел [tool.hatch.build.targets.wheel]
Зависимости
После команды открой pyproject.toml — в разделе dependencies появились две строки:
Заодно появился uv.lock и папка .venv. Загляни в uv.lock — он намного больше двух строк. Пойми, почему.
uv add fastapi uvicorn- В pyproject.toml в dependencies появились fastapi и uvicorn
- Создан файл uv.lock
- Ты можешь объяснить, почему uv.lock гораздо больше списка в pyproject
Первая победа: пакет установлен и импортируется.
Попробуй ради эксперимента ту же строку без uv run — просто python -c "import fastapi". Скорее всего упадёт с ModuleNotFoundError, и это правильно.
uv run python -c "import fastapi; print(fastapi.__version__)"- Команда напечатала версию fastapi
- Ты попробовал то же без uv run и видел разницу
Флаг --dev кладёт зависимость в отдельный раздел, а не в основной:
Сравни с тем, куда попали fastapi и uvicorn. Доки: ruff
uv add --dev ruff- ruff оказался в разделе dev, а не в dependencies
- Ты можешь объяснить, почему это разные списки
Команды проекта
Создай папку scripts/ с пустым __init__.py и файлом commands.py:
Обрати внимание на sys.exit(...): код возврата пробрасывается наружу.
- Файл scripts/commands.py создан
- В папке scripts есть __init__.py
- Функция возвращает код через sys.exit
Добавь раздел (если uv уже создал его с примером — замени содержимое):
Слева от знака равенства — имя команды, справа — путь к функции в формате модуль:функция.
- В pyproject.toml есть раздел [project.scripts] с командой lint
- Путь указан через двоеточие: модуль:функция
При первом запуске uv доустановит сам проект в окружение — увидишь строку вида + имя-проекта==0.1.0 (from file:///...). Это нормально и именно так рождаются команды.
Ожидаемый результат — All checks passed!
Если пишет Failed to spawn: lint — значит нет раздела [build-system] или сборщик не видит папку scripts. Вернись к шагам про --package и [tool.hatch.build.targets.wheel].
uv run lint- Команда отработала и напечатала результат проверки
- В выводе было видно, как устанавливается сам проект
Команда печатает путь к тому файлу, который реально импортируется. Должен быть scripts/commands.py.
Запомни эту команду — она пригодится каждый раз, когда «код точно правильный, а ведёт себя странно».
uv run python -c "import importlib.util as u; print(u.find_spec('scripts.commands').origin)"- Напечатан путь к scripts/commands.py
- В папке scripts нет одновременно файла и папки с одинаковым именем
Git
uv уже создал .gitignore. Проверь, что в нём есть .venv, и что там нет uv.lock.
Минимум:
Идёт в git: pyproject.toml, uv.lock, .python-version, код.
Не идёт: .venv, __pycache__, .env.
- .venv перечислен в .gitignore
- uv.lock В .gitignore не перечислен
- .env перечислен, даже если файла ещё нет
В списке должны быть pyproject.toml, uv.lock, .python-version, .gitignore, папки app и scripts.
Папки .venv в списке быть не должно. Если она там есть — ты успел добавить её в git до того, как появился .gitignore. Лечится: git rm -r --cached .venv
git status- .venv не попадает в список
- uv.lock и .python-version в списке есть
В боевом проекте, на который мы ориентируемся, — 978 файлов .py, и всё окружение поднимается одной командой. В корне лежат ровно те же три файла, что ты только что сделал.
Команд там четырнадцать: dev, serve, worker, test, lint, format, migrate, deploy и другие. Новый человек не выясняет, чем запускать проект, — он смотрит список команд.
И что там вышло неудачно. Рядом лежат файл scripts/commands.py и пакет scripts/commands/ — с одинаковым именем. Python выбирает пакет, поэтому файл не импортируется никогда. Мёртвый код, который выглядит рабочим и вводит в заблуждение при чтении.
Второе: команда bootstrap описана как «полная инициализация проекта», а вызывает функцию activate — то есть делает не то, что заявлено.
Мы такого не повторяем: имя пакета и имя модуля в одной папке не совпадают, а команда делает ровно то, что написано в её описании. Поэтому шаг с find_spec выше — не формальность.
Добавь вторую команду — format, которая запускает ruff format . и точно так же пробрасывает код возврата.
Обрати внимание: назвать функцию просто format нельзя — так уже называется встроенная функция Python, и ты её затенишь. Назови format_code, а имя команды оставь format:
Критерий готовности: uv run lint и uv run format работают и делают разное. Создай файл с кривыми отступами и убедись, что первая ругается, а вторая правит.
У тебя проект, который восстанавливается одной командой на любой машине, с закреплённой версией Python и со своими командами.
Кода пока нет — и это нормально. Сначала строится то, во что код будет ложиться.
Что дальше: структура проекта — пять слоёв внутри app/ и правило, по которому они зависят друг от друга. После этого — настройки, точка входа и первый эндпоинт.

