placeholderPython开发环境与工具链:VSCode、UV、Ruff

Python开发环境与工具链:VSCode、UV、Ruff

从解释器、虚拟环境、依赖管理、编辑器配置到 Ruff 检查格式化,系统搭建一套现代 Python 开发工作流,并解释每一步背后的取舍。

Python 的开发环境最容易让人困惑的地方,不是某个命令很难,而是工具太多:系统自带 Python、官网下载 Python、conda、venv、pip、pipx、poetry、pdm、pyenv、ruff、mypy、pytest、VS Code 插件。每个工具都能解决一部分问题,但边界不清楚时,项目很快就会变成“能跑,但是不知道为什么能跑”。

这篇文章按我更推荐的心智模型整理一套日常开发工具链:

  • uv 管 Python 版本、虚拟环境和依赖。
  • pyproject.toml 放项目元数据和工具配置。
  • 用 VS Code + Pylance 负责编辑器补全、跳转和类型提示。
  • 用 Ruff 负责格式化、导入排序和静态检查。

目标不是追求工具新,而是让环境可复现、命令少、项目根目录干净,并且换一台机器后也能尽量少猜。

uv 吸引人的地方首先是快,但更重要的是它把过去分散在 pippip-toolsvirtualenvpipxpyenv 和部分项目管理工具里的能力收束到一个命令下。速度只是入口,统一的工作流才是长期收益。

如果只是偶尔写脚本,python -m venvpip 仍然能工作;如果你维护的是需要多人协作、持续测试、锁定依赖和统一格式的项目,uv + Ruff + pyproject.toml 会更省心。

一套环境要解决什么问题

一个 Python 项目至少有四层状态:

层级典型问题推荐承载方式
Python 解释器项目到底用 3.10、3.11 还是 3.12?.python-versionrequires-pythonuv python
虚拟环境依赖安装到哪里,是否污染系统环境?.venv
依赖声明项目直接依赖哪些库?开发依赖有哪些?pyproject.toml
依赖锁定每个库最终解析到哪个精确版本?uv.lock 或导出的 requirements.txt

过去常见的问题是这四层混在一起。例如 pip install requests 会把包安装进当前环境,但不会自然回答“这个包应该写入哪里”“别人如何复现”“Python 版本不一致怎么办”。uv 的项目模式把这些问题串起来:

uv init democd demouv add requestsuv run python main.py

运行这些命令后,项目里会逐步出现:

这里的重点不是文件多了,而是职责清楚了:

  • .python-version 说明这个目录希望使用哪个 Python。
  • .venv 是项目本地虚拟环境,不应该提交到 Git。
  • pyproject.toml 记录项目元数据和直接依赖。
  • uv.lock 记录完整解析结果,适合提交到 Git 以保证协作机器一致。

安装 Uv

官方文档入口是 uv docs。独立安装器不依赖本机已有 Python,更适合作为第一选择。

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

安装完成后重新打开终端,或者按安装器提示刷新 shell 配置。然后检查版本:

uv --version
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

安装完成后重新打开 PowerShell,然后检查版本:

uv --version

如果环境里已经有可用的 Python,也可以用 pip 安装:

pip install uv

这种方式适合临时机器或 CI 镜像,但长期个人机器更推荐独立安装器,因为它不依赖某个特定 Python 环境。

管理 Python 版本

Python 项目最常见的隐性 bug,是作者本地用 3.12,CI 或同事机器用 3.10。语法、标准库、类型注解和依赖解析都可能因此不同。

先查看可用版本:

uv python list

安装指定版本:

uv python install 3.12

在当前项目固定 Python 版本:

uv python pin 3.12

这个命令会写入 .python-version。之后在项目目录里执行 uv runuv sync 等命令时,uv 会优先按这个版本找解释器;如果找不到,在允许下载的情况下会自动下载对应版本。

还可以在 pyproject.toml 里声明项目支持的 Python 范围:

[project]requires-python = ">=3.12"

.python-versionrequires-python 不是一回事:

文件 / 字段作用
.python-version本地工作目录偏好的解释器版本,偏向开发体验
project.requires-python项目声明支持的 Python 版本范围,偏向包元数据和依赖解析

个人项目里可以两者都写。例如要求项目支持 >=3.12,本地当前固定在 3.123.13

创建项目

新项目推荐直接用 uv init

uv init my-appcd my-app

如果当前目录已经是项目目录:

uv init

运行示例程序:

uv run python main.py

第一次执行项目命令时,uv 会创建 .venv、解析依赖,并生成 uv.lock。和手动激活虚拟环境相比,uv run 的好处是命令更明确:你是在“项目环境”里运行这个命令,而不是依赖当前 shell 是否已经激活了正确环境。

例如:

uv run python --versionuv run python -c "import sys; print(sys.executable)"

第二条命令应该输出项目 .venv 里的 Python 路径。这是检查环境是否正确的最直接方法。

虚拟环境:要不要手动激活

uv 支持两种使用方式。

第一种是不激活环境,所有命令都加 uv run

uv run python main.pyuv run pytestuv run ruff check .

这适合脚本、CI 和文档,因为命令自带上下文,不依赖 shell 状态。

第二种是激活 .venv,之后直接运行命令:

source .venv/bin/activatepython main.pydeactivate
.\.venv\Scripts\Activate.ps1python main.pydeactivate
overlay use .venv/bin/activate.nupython main.pydeactivate

Windows 下 Nushell 的路径通常是:

overlay use .venv/Scripts/activate.nu

我的习惯是:日常终端里可以激活,文档和自动化脚本里尽量用 uv run。这样不会把“我当前 shell 的状态”变成项目构建的一部分。

添加、更新和删除依赖

添加运行时依赖:

uv add requests

添加开发依赖:

uv add --dev pytest ruff

指定版本:

uv add "ruff==<version>"uv add "fastapi>=0.115"

删除依赖:

uv remove requestsuv remove --dev ruff

查看依赖树:

uv tree

这里需要区分 uv adduv pip install

命令适合场景是否写入项目依赖
uv add <pkg>项目依赖管理是,写入 pyproject.toml 并更新锁文件
uv pip install <pkg>兼容旧的 pip 工作流或临时环境操作通常不改项目元数据
uv run --with <pkg> <cmd>临时带一个包运行命令不持久写入项目

例如临时运行一个工具:

uv run --with rich python -c "from rich import print; print('[green]hello[/green]')"

如果这个包是项目长期依赖,就不要用临时命令,应该 uv add rich

同步和锁定依赖

协作项目里,推荐提交 uv.lock。别人拉取项目后运行:

uv sync

uv sync 会让 .venv 和项目声明、锁文件保持一致。根据官方 CLI 说明,项目环境不存在时会创建;默认同步会移除未声明的额外包,所以它比“只安装缺的包”更适合复现环境。

常用命令:

# 只更新锁文件uv lock# 根据锁文件同步环境uv sync# 同步后运行命令uv run pytest

如果你需要给不使用 uv 的环境提供 requirements.txt,可以导出:

uv export --format requirements-txt --output-file requirements.txt

老的 pip-tools 风格也可以用:

uv pip compile pyproject.toml -o requirements.txtuv pip sync requirements.txt

选择哪一种,取决于项目边界:

  • 项目内部开发:优先 uv.lock + uv sync
  • 部署平台只认 requirements.txt:用 uv exportuv pip compile 生成。
  • 旧项目迁移:先保留 requirements.txt,用 uv pip 接管安装速度,再慢慢迁移到 pyproject.toml

VS Code 插件

推荐安装这些插件:

  • Python
  • Pylance
  • Ruff
  • Python Debugger
  • Python postfix completion

各自职责不同:

插件主要职责
Python选择解释器、运行测试、调试入口
Pylance语言服务、跳转、补全、类型检查
Ruff格式化、导入排序、Lint 诊断和自动修复
Python Debugger调试 Python 程序
Python postfix completion后缀补全,提升编辑效率

安装插件后,先在 VS Code 命令面板里选择解释器:

Python: Select Interpreter

选择项目里的 .venv。如果没有看到 .venv,先在项目根目录运行:

uv sync

VS Code 保存时格式化和修复

在项目级 .vscode/settings.json 中写配置,比写全局配置更可复现。一个基础配置如下:

{  "editor.formatOnPaste": true,  "editor.formatOnSave": true,  "editor.formatOnType": true,  "[python]": {    "editor.defaultFormatter": "charliermarsh.ruff",    "editor.codeActionsOnSave": {      "source.fixAll.ruff": "always",      "source.organizeImports.ruff": "always"    }  }}

这段配置做三件事:

  • 保存时调用 Ruff formatter。
  • 保存时用 Ruff 执行可自动修复的 lint。
  • 保存时整理 import。

如果项目已经使用 Black,也可以用微软维护的 Black Formatter 插件。但对新项目来说,用 Ruff 同时负责格式化和 lint,配置更少,速度也更一致。

Pylance 配置

Pylance 负责“编辑时理解代码”。它和 Ruff 的边界要分清楚:

  • Ruff 更像快速代码质量工具:格式、导入、常见错误、风格规则。
  • Pylance 更像语言智能:类型推断、跳转、补全、引用查找。

可以从下面这份配置开始:

{  "python.languageServer": "Pylance",  "python.analysis.cacheLSPData": true,  "python.analysis.autoFormatStrings": true,  "python.analysis.autoImportCompletions": true,  "python.analysis.completeFunctionParens": true,  "python.analysis.typeCheckingMode": "basic",  "editor.inlayHints.enabled": "offUnlessPressed",  "python.analysis.inlayHints.callArgumentNames": "all",  "python.analysis.inlayHints.functionReturnTypes": true,  "python.analysis.inlayHints.pytestParameters": true,  "python.analysis.inlayHints.variableTypes": true,  "python.terminal.activateEnvInCurrentTerminal": true,  "python.terminal.shellIntegration.enabled": true}

typeCheckingMode 有几个常用值:

适合场景
off只想要补全和跳转,不想看到类型报错
basic日常项目推荐起点
strict类型约束较强的项目,或者核心库、长期维护项目

我不建议一上来就给所有项目开 strict。严格类型检查很有价值,但它也会暴露很多历史代码问题。如果项目已有大量动态代码,可以先从 basic 开始,把新增模块写干净,再逐步提高标准。

Ruff:格式化和静态检查

Ruff 是 Astral 写的 Python linter 和 formatter。它常见的替代对象包括 Flake8、isort、部分 pyupgrade 规则和 Black 的一部分格式化工作。

安装到项目开发依赖:

uv add --dev ruff

手动运行检查:

uv run ruff check .

自动修复可修复问题:

uv run ruff check . --fix

格式化代码:

uv run ruff format .

检查格式但不改文件,适合 CI:

uv run ruff format . --check

Ruff 的配置可以放在 pyproject.tomlruff.toml.ruff.toml。项目已有 pyproject.toml 时,直接集中到里面通常更清爽。

[tool.ruff]line-length = 100target-version = "py312"respect-gitignore = trueindent-width = 4[tool.ruff.format]quote-style = "double"indent-style = "space"docstring-code-format = true[tool.ruff.lint]select = [  "E",    # pycodestyle error  "F",    # pyflakes  "I",    # isort  "UP",   # pyupgrade  "B",    # flake8-bugbear  "SIM"   # flake8-simplify]ignore = []

几点经验:

  • line-length = 100 对现代屏幕比较舒服,团队也常用 88 或 120,关键是统一。
  • target-version 应该和项目支持的 Python 版本一致。Ruff 会据此决定能否使用较新的语法改写。
  • select 不要一次开太激进。先从 E/F/I/UP/B 开始,稳定后再扩。
  • 如果你已经在 [project] 里写了 requires-python,Ruff 官方也能从中推断目标版本;显式写 target-version 时,以 Ruff 配置为准。

Git Hook:推送前检查

项目里最好把检查命令固化下来。最简单的方式是写一个 pre-push hook:

#!/bin/shuv run ruff format . --checkuv run ruff check .uv run pytest

保存为:

.git/hooks/pre-push

然后给执行权限:

chmod +x .git/hooks/pre-push

这段脚本故意不自动修复,因为 push 阶段应该负责拦截问题,而不是悄悄修改工作区。自动修复更适合在编辑器保存时或手动执行:

uv run ruff check . --fixuv run ruff format .

如果团队协作,建议用 pre-commit 框架或 CI,而不是只依赖本地 .git/hooks,因为 Git hook 默认不会随仓库克隆自动启用。

pyproject.toml 示例

pyproject.toml 是现代 Python 项目的中心配置文件。它来自 PEP 518 之后的生态演进,用来减少 setup.pysetup.cfgrequirements.txt、各种工具配置文件分散的问题。

一个常见项目可以这样写:

[project]name = "vnet"version = "0.1.0"description = "Virtual network playground"readme = "README.md"requires-python = ">=3.12"dependencies = [  "pydantic>=2.10",][dependency-groups]dev = [  "pytest>=8.3",  "pytest-cov>=6.0",  "ruff",]doc = [  "mkdocs-material>=9.5",  "mkdocs-mermaid2-plugin>=1.1",][tool.ruff]line-length = 100target-version = "py312"respect-gitignore = trueindent-width = 4[tool.ruff.format]quote-style = "double"indent-style = "space"docstring-code-format = true[tool.ruff.lint]select = ["E", "F", "I", "UP", "B", "SIM"]ignore = ["E402"][tool.pytest.ini_options]addopts = "-ra"testpaths = ["tests"]python_files = ["test_*.py"]python_classes = ["Test*"]python_functions = ["test_*"]log_cli = truelog_cli_format = "%(asctime)s [%(levelname)s] %(message)s"log_cli_date_format = "%m.%d %H:%M:%S"

这里有几个容易踩坑的点:

  • dependencies 放运行时依赖,也就是你的包真正运行需要的库。
  • dev 这类开发依赖不要混进运行时依赖,否则部署环境会变重。
  • pythonpath 不建议随手写成个人机器绝对路径,例如 /home/name/workspace/project。这会让配置只在你的电脑上生效。更推荐采用标准包结构,或者在测试命令中明确设置。
  • indent-style = "tab" 虽然 Ruff 支持,但 Python 社区更常见的是 space。除非团队明确要求 tab,否则不建议改。

一个更完整的项目骨架

如果项目不只是单文件脚本,推荐用 src layout:

这样可以避免“当前目录刚好在 sys.path 里,所以测试能过,安装后反而不能 import”的问题。

示例:

# src/vnet/main.pydef add(left: int, right: int) -> int:    return left + right
# tests/test_main.pyfrom vnet.main import adddef test_add() -> None:    assert add(1, 2) == 3

运行:

uv run pytestuv run ruff check .uv run ruff format . --check

如果三条命令都能过,这个项目的基本开发闭环就成立了。

__pycache__ 和临时文件

Python 会把字节码缓存写到 __pycache__,pytest 会写 .pytest_cache,Ruff 会写 .ruff_cache。这些目录一般不需要提交。

.gitignore 至少写:

.venv/__pycache__/*.py[cod].pytest_cache/.ruff_cache/.mypy_cache/.coveragehtmlcov/dist/build/

需要清理时:

find . -type d -name "__pycache__" -prune -exec rm -r {} +find . -type d -name ".pytest_cache" -prune -exec rm -r {} +find . -type d -name ".ruff_cache" -prune -exec rm -r {} +find . -type f -name "*.pyc" -delete

Windows PowerShell:

Get-ChildItem -Recurse -Directory -Filter "__pycache__" | Remove-Item -Recurse -ForceGet-ChildItem -Recurse -Directory -Filter ".pytest_cache" | Remove-Item -Recurse -ForceGet-ChildItem -Recurse -Directory -Filter ".ruff_cache" | Remove-Item -Recurse -ForceGet-ChildItem -Recurse -File -Filter "*.pyc" | Remove-Item -Force

清理缓存不是日常必须操作。通常只有在路径混乱、测试行为奇怪、或者打包前想保持目录干净时才需要。

dataclass:小而常用的数据建模工具

在 Python 中,定义一个只负责承载数据的类,经常会写重复代码:__init____repr____eq__dataclass 用装饰器帮你生成这些样板代码。

from dataclasses import dataclass@dataclassclass User:    name: str    age: intuser = User("alice", 18)print(user)

输出类似:

User(name='alice', age=18)

如果希望对象创建后不可变:

from dataclasses import dataclass@dataclass(frozen=True)class Point:    x: int    y: intp = Point(10, 20)# p.x = 30  # 会报错,因为 frozen=True 禁止修改字段

需要隐藏敏感字段:

from dataclasses import dataclass, field@dataclassclass Account:    username: str    password: str = field(repr=False)account = Account(username="alice", password="secret")print(account)

输出不会包含密码:

Account(username='alice')

需要转成字典:

from dataclasses import asdict, dataclass@dataclassclass Point:    x: int    y: intpayload = asdict(Point(1, 2))

dataclass 适合轻量数据对象。如果你需要复杂校验、JSON schema、API 入参出参模型,Pydantic 往往更合适;如果只是内部函数之间传递结构化数据,dataclass 已经足够。

日常命令清单

下面这组命令基本覆盖个人项目从初始化到检查的流程:

# 创建项目uv init my-appcd my-app# 固定 Python 版本uv python pin 3.12uv sync# 添加依赖uv add requestsuv add --dev pytest ruff# 运行uv run python main.pyuv run pytest# 检查和格式化uv run ruff check .uv run ruff check . --fixuv run ruff format .# 查看依赖uv tree# 同步环境uv sync

该怎么取舍

如果只是一次性脚本,最小可用方案是:

uv run --with requests script.py

如果是长期维护项目,推荐:

uv inituv add --dev pytest ruff

然后把 pyproject.tomluv.lock.python-version.vscode/settings.json 维护好。

如果是已有老项目,不要急着一次性替换所有工具。可以按这个顺序迁移:

  1. 先用 uv pip sync requirements.txt 替代慢速安装。
  2. 再把 Ruff 接进编辑器和 CI。
  3. 最后把依赖声明迁到 pyproject.toml,用 uv.lock 固化解析结果。

工具链的理想状态不是“每个工具都最新”,而是每个问题只有一个明确答案:Python 版本在哪里定,依赖在哪里声明,环境怎么复现,格式和检查由谁负责。做到这一步,Python 项目就会少很多玄学。

References

Display Settings

CompactRelaxed
Normal1.70

评论