《Python编程实战》1.1 从零搭建:uv + ruff + 静态检查

真实项目的第一步不是写代码,而是把脚手架搭对。本节用 uv 从零建出一个 src 布局的 Python 项目,实测 uv init、uv run、uv add、uv lock 的完整输出,再把 ruff 与 mypy 装成 dev 依赖跑通第一次静态检查,让你五分钟拿到一个可提交、可协作、可复现的工程骨架。

本节目标:用 uv 从零建出一个标准 Python 项目,跑通「建项目 → 管依赖 → 装工具 → 静态检查」这条最短闭环;读完后你能独立搭起一个可提交、可协作的工程骨架。
适用版本:Python 3.12+(实测 3.14.6);uv 0.12.23、ruff 0.16.10、mypy 2.4.0

1.1 从零搭建:uv + ruff + 静态检查

很多教程从 print("hello") 开始,但真实项目的第一个动作往往相反:先把工程骨架立起来,再往里填代码。骨架决定了依赖怎么锁、检查怎么跑、别人克隆后能不能一键复现。本节就做这一件事——用 uv 从零搭一个项目,并让静态检查真正跑起来。

先看清要替代的是哪几件旧工具

在 uv 出现之前,搭一个 Python 项目要拼四五个工具:pip 装包、venv 建环境、pyenv 管解释器、pip-tools 或 poetry 锁依赖。它们各自能跑,但拼在一起就是四套配置、四种心智模型。uv 把它们收进一个二进制:

旧工具职责uv 对应命令
pyenv安装/切换 Python 解释器uv python install / uv python pin
venv / virtualenv创建虚拟环境uv venv
pip安装包uv pip install / uv add
pip-tools / poetry解析并锁定依赖uv lock / uv sync

这不是「多一个选择」,而是把原本分散的四件事收敛成一条命令链。后面你会看到,uv run 甚至让「激活虚拟环境」这一步都省掉了。

安装 uv 并确认版本

uv 是单文件二进制,官方安装脚本一行搞定:

curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version

本机实测输出(2026-10-09):

uv 0.12.23 (46b84fd0b 2026-10-03 aarch64-apple-darwin)

版本号里带了构建日期与目标平台,排查问题时先看这行——团队里 uv 版本差太多,锁文件格式可能对不上。

uv init:五分钟得到一个骨架

在空目录里执行:

uv init demo

它会打印项目落点并直接生成文件:

Initialized project `demo` at `/private/tmp/python_book/scratch/01/demo`

生成的目录结构如下(find demo -type f 实测):

demo/
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
└── src/
    └── demo/
        └── __init__.py

注意它默认采用 src 布局:源码藏在 src/demo/ 里,项目根不直接暴露包。这个设计逼你在开发阶段就按「安装后的形态」导入,能提前暴露打包漏文件的坑。uv init 还会顺手初始化 Git 仓库、写好 .gitignore。

生成的 pyproject.toml(作者信息由本机 Git 配置填入,这里照实显示):

[project]
name = "demo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
    { name = "leting.yan", email = "leting.yan@plumephp.com" }
]
requires-python = ">=3.14"
dependencies = []

[project.scripts]
demo = "demo:main"

[build-system]
requires = ["uv_build>=0.12.23,<0.13.0"]
build-backend = "uv_build"

这里有两处要留意。一是 requires-python 跟随了本机解释器写成 >=3.14;本书基线是 >=3.12,实际项目里应手动改成 ">=3.12" 以获得更宽的兼容面。二是 [build-system] 用了 uv 自家的 uv_build 后端,不需要额外的构建依赖。[project.scripts] 注册了一个叫 demo 的命令行入口,指向 demo:main——也就是 src/demo/__init__.py 里的 main 函数。

uv run:不激活环境也能跑

传统流程要 source .venv/bin/activate 再 python,uv 把这两步合成一步:

uv run demo

首次运行会自动创建虚拟环境、按需安装依赖、构建本地包,然后执行入口:

Using CPython 3.14.6 interpreter at: /opt/homebrew/opt/python@3.14/bin/python3.14
Creating virtual environment at: .venv
   Building demo @ file:///private/tmp/python_book/scratch/01/demo
      Built demo @ file:///private/tmp/python_book/scratch/01/demo
Installed 1 package in 38ms
Hello from demo!

uv run 会在执行前把环境同步到与锁文件一致的状态,所以「在我机器上能跑」这句话的适用范围被显著拉大了:只要 uv.lock 进了版本库,换台机器 uv run 就会还原出一模一样的依赖。

uv add 与 uv.lock:依赖怎么进项目

加一个运行时依赖,用 uv add:

uv add "idna==3.20"
Resolved 2 packages in 25ms
   Building demo @ file:///private/tmp/python_book/scratch/01/demo
      Built demo @ file:///private/tmp/python_book/scratch/01/demo
Prepared 1 package in 78ms
Uninstalled 1 package in 24ms
Installed 2 packages in 71ms
 ~ demo==0.1.0 (from file:///private/tmp/python_book/scratch/01/demo)
 + idna==3.20

uv add 一次做三件事:写进 pyproject.toml 的 dependencies、解析并更新 uv.lock、把包装进 .venv。项目里的 dependencies 段于是变成:

dependencies = [
    "idna==3.20",
]

uv.lock 记录的是整个依赖图的精确版本,而不只是你直接写的那几个。本机在无依赖时 uv lock 生成的最小锁文件长这样:

version = 1
revision = 5
requires-python = ">=3.14"

[[package]]
name = "demo"
version = "0.1.0"
source = { editable = "." }

锁文件必须提交——它是团队复现环境的依据;.venv/ 则必须忽略,因为里面装的是本机二进制。

把 ruff 与 mypy 装成 dev 依赖

代码检查工具只在开发和 CI 用,不该进运行时依赖。用 --dev 分组:

uv add --dev "ruff==0.16.10" "mypy==2.4.0"

uv 0.12.23 会写入 PEP 735 的依赖组,而不是塞进 dependencies:

[dependency-groups]
dev = [
    "mypy==2.4.0",
    "ruff==0.16.10",
]

这样 uv sync 默认会装 dev 组,而部署时可以用 uv sync --no-dev 只装运行时依赖,把镜像体积和攻击面都压下来。

第一次静态检查

工具装好了,跑起来看真实结果。先让 ruff 检查源码:

uv run ruff check src
All checks passed!

再让 mypy 做类型检查:

uv run mypy src
Success: no issues found in 1 source file

两条命令都通过,说明骨架是干净的。故意写点问题代码试试门禁是否真的会拦——这是后面提交门禁的基础:

import os


def add(a, b):
    return a + b
uv run ruff check .
F401 [*] `os` imported but unused
 --> bad.py:1:8
  |
1 | import os
  |        ^^
help: Remove unused import: `os`
  |

Found 1 error.
[*] 1 fixable with the `--fix` option.

ruff 精确指出了「第 1 行导入了 os 却没用」,还标出这一条可以自动修复。静态检查的价值正在于此:把一类低级错误从「运行时才炸」提前到「保存时就报」。

本机实测工具矩阵

本节涉及的工具体积小、速度快,是整套工程链的地基。下表版本均为 2026-10-09 在本机实测:

工具实测版本职责
uv0.12.23解释器管理、虚拟环境、依赖解析与锁定
ruff0.16.10lint + format 二合一
mypy2.4.0静态类型检查
CPython3.14.6运行时解释器

一句话概括这条链:uv 管「环境与依赖」,ruff 管「代码风格与常见错误」,mypy 管「类型正确性」。三者职责不重叠,配置都收敛进 pyproject.toml 一个文件。

小结

  • uv 一个二进制替代了 pyenv + venv + pip + pip-tools 四件工具,命令链更短、速度更快。
  • uv init 默认生成 src 布局项目,并顺手写好 .gitignore 与 Git 仓库。
  • uv run 免激活环境即可执行,且每次执行前把环境同步到与 uv.lock 一致。
  • uv add 同时更新 pyproject.toml、uv.lock 与虚拟环境;--dev 写入 [dependency-groups],与运行时依赖隔离。
  • uv.lock 必须提交、.venv/ 必须忽略,这是可复现构建的前提。
  • ruff 与 mypy 的第一次运行应当全绿;一旦报错,它们会精确到文件、行、列与规则编号。

骨架搭好了,但 pyproject.toml 里现在只填了最少的字段。下一节 1.2 pyproject.toml 全解与类型检查分层 会把这份配置拆到每一行,并解决一个实战难题——如何让核心模块严格、遗留模块宽松,而不是一刀切。想先看工具链的完整全景,可以读专题文章 现代 Python 工具链 。

阅读导航:上一节:《Python编程实战》目录 · 下一节:1.2 pyproject.toml 全解与类型检查分层 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时