关于 python-dev
python-dev 是用于创建或现代化 Python 项目的配置 Skill,规定依赖管理、类型检查、代码检查、测试和任务命令所用工具。它还提供配置模板和迁移指引。
主要功能
- 01基于 uv 的项目与依赖配置
- 02ty 类型检查配置
- 03ruff 代码检查与格式化
- 04pytest 与 just 工作流
- 05lefthook Git 钩子配置
适用场景
- 01配置新的 Python 项目
- 02将现有项目迁移到该工具栈
- 03为开发工作流添加检查和测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tenequm/ skills --skill python-dev安装后在 Claude Code 中输入/python-dev确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
CHANGELOG.mdLICENSE.txtreferences/justfile-reference.mdpytest-reference.mdruff-reference.mdty-reference.mduv-reference.md详细介绍
Python 开发配置
注重实践、可用于生产环境的 Python 开发技术栈。无需选择,直接使用即可。
适用场景
- 开始一个新的 Python 项目
- 现代化改造现有项目(从 pip/poetry/mypy/black/flake8 迁移)
- 设置 lint、格式化、类型检查或测试
- 为项目命令创建 Justfile
- 将 pyproject.toml 配置为唯一可信来源
技术栈
| 工具 | 用途 | 替代工具 |
|---|---|---|
| uv 0.12+ | 包管理器、Python 版本管理器、运行器 | pip、poetry、pyenv、virtualenv |
| ty(测试版) | 类型检查器(Astral,Rust) | mypy、pyright |
| ruff | Linter + 格式化工具 | flake8、black、isort、pyupgrade |
| pytest | 测试 | unittest |
| just | 命令运行器 | make |
| lefthook 2.1+ | Git 钩子(单个二进制文件,并行运行) | pre-commit |
关于 ty 的说明:ty 目前处于测试阶段(0.0.x),没有稳定的 API,推断行为也可能在任意两个版本间发生变化,因此请锁定版本。Pydantic 已不再是合理的反对理由:从 0.0.57 起,ty 已为其推出专门的库支持路线(构造函数、
model_config、BaseSettings、RootModel、严格与宽松模式)。Django 和 SQLAlchemy 仍不受此类支持,因此很可能产生误报。在更换整个检查器之前,可以先使用[tool.ty.analysis] replace-imports-with-any = ["sqlalchemy.**"],这样只会屏蔽一个有问题的依赖,而不是全部依赖。如果你现在确实需要稳如磐石的检查,请将ty换成pyright,其余技术栈保持不变。
快速开始:新建项目
# 1. Create project with src layout (uv 0.12+ packages by default; --package is redundant)
uv init my-project
cd my-project
# 2. Pin Python version
uv python pin 3.13
# 3. Add dev dependencies
uv add --dev ruff ty pytest pytest-asyncio lefthook
# 4. Create Justfile and lefthook.yml (see templates below)
# 5. Configure pyproject.toml (see template below)
# 6. Install git hooks
uv run lefthook install
# 7. Run checks
just check
pyproject.toml 模板
这是唯一的配置文件。复制它并调整 [project] 字段。
[project]
name = "my-project"
version = "0.1.0"
description = "Project description"
readme = "README.md"
requires-python = ">=3.13"
license = {text = "MIT"}
dependencies = []
[project.scripts]
my-project = "my_project:main" # CLI: `uv run my-project` -> main() in src/my_project/__init__.py
[dependency-groups]
dev = [
"ruff>=0.16.6", # 0.16 changed the default rule set - see the lint note below
"ty>=0.0.79", # beta: pin tight, inference changes between minors
"pytest>=9.1.1", # 9.1 fixed addopts strictness being ignored
"pytest-asyncio>=1.4.0", # 1.4 added the loop-factories hook
"lefthook>=2.1.12",
]
# uv 0.12+ generates `uv_build` here instead. Keep that unless you need a hatchling
# plugin - this block is a deliberate override, not what `uv init` gives you.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/my_project"]
# =============================================================================
# RUFF - Loose, helpful rules only
# =============================================================================
[tool.ruff]
target-version = "py313"
line-length = 100
[tool.ruff.lint]
# Ruff 0.16 enables 413 rules by default (up from 59). Do NOT write a `select`
# list here unless you mean to shrink that - `select = ["E","F","I","UP"]` now
# makes ruff weaker than no config at all. Narrow with `extend-select`/`ignore`.
ignore = [
"E501", # line too long - formatter handles it
"UP007", # X | Y unions - Optional[X] is more readable
]
exclude = [".git", ".venv", "__pycache__", "build", "dist"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "lf"
# Ruff 0.16 formats Python blocks inside Markdown by default. Drop this line
# only if you want README code fences reformatted too.
exclude = ["*.md"]
# =============================================================================
# TY - Type Checker
# =============================================================================
[tool.ty.environment]
python-version = "3.13"
[tool.ty.src]
include = ["src"]
# =============================================================================
# PYTEST
# =============================================================================
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
asyncio_mode = "auto"
# pytest 9.0 silently ignored --strict-markers/--strict-config passed via
# addopts. Use the ini keys, which work on 9.0 and 9.1 alike. `strict = true`
# is the shorthand and also covers strict_xfail + strict_parametrization_ids.
strict = true
addopts = ["-ra"]
上面的配置有意不设置 asyncio_default_fixture_loop_scope;pytest-asyncio 每次运行时都会对此发出警告。将其设为 "function" 即可消除警告并固定行为。
Justfile 模板
# Check types, lint, and formatting (non-mutating; mirrors CI)
check:
uv run ty check
uv run ruff check
uv run ruff format --check
# Run tests
test *ARGS:
uv run pytest {{ARGS}}
# Run tests with coverage
test-cov:
uv run pytest --cov=src --cov-report=term-missing
# Auto-fix and format
fix:
uv run ruff check --fix
uv run ruff format
# Install/sync all dependencies
install:
uv sync --all-groups
uv run lefthook install
# Update all dependencies
update:
uv lock --upgrade
uv sync --all-groups
# Clean build artifacts
clean:
rm -rf dist/ build/ .pytest_cache/ .ruff_cache/ htmlcov/
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
Lefthook 配置
这里用 Lefthook 替代 pre-commit,原因与 go-dev 使用它相同:只有一个二进制文件、钩子并行运行,而且——这在 Python 中最重要——钩子运行的是来自 uv.lock 的你的 ruff,而不是在钩子配置中单独锁定的另一个副本。它以平台 wheel 的形式从 PyPI 安装,因此 uv add --dev lefthook 就完成了全部安装;无需 Go 工具链。
uv run lefthook install # writes .git/hooks/pre-commit and pre-push
# lefthook.yml
assert_lefthook_installed: true
pre-commit:
piped: true # a failed job stops the rest; lint must fix before format runs
jobs:
- name: guards
group:
parallel: true
jobs:
- name: private-key
run: "! grep -lE 'BEGIN [A-Z ]*PRIVATE KEY' {staged_files}"
- name: merge-conflict
run: "! grep -lE '^(<<<<<<<|>>>>>>>) ' {staged_files}"
- name: large-files
exclude: ["uv.lock"]
run: "! find {staged_files} -type f -size +1000k | grep ."
- name: ruff-check
glob: "*.{py,pyi,ipynb}"
run: uv run ruff check --force-exclude --fix {staged_files}
stage_fixed: true
- name: ruff-format
glob: "*.{py,pyi,ipynb}"
run: uv run ruff format --force-exclude {staged_files}
stage_fixed: true
- name: ty
run: uv run ty check # no glob - see the glob trap below
pre-push:
jobs:
- name: test
run: uv run pytest
--force-exclude 是必需项,不是装饰。 对于通过命令行显式传入的路径,Ruff 会忽略自己的 exclude 配置,而 {staged_files} 会显式传入路径。没有这个标志时,[tool.ruff] exclude 下的暂存文件仍会被 lint——经验证:加上这个标志,ruff check --force-exclude src/generated/gen.py 会报告 All checks passed;不加时,同一条命令会发现错误。pre-commit 用户从未遇到这个问题,因为 ruff-pre-commit 已将此标志写入钩子条目;使用 lefthook 时,需要你自己记住加上它。
stage_fixed: true 是相比 pre-commit 更方便之处。 pre-commit 在钩子改写文件时会使提交失败,并要求你重新暂存并再次运行。lefthook 会对修复后的文件重新运行 git add,然后继续提交。从 2.1.12 起,如果 git add 失败,钩子也会失败,因此修复结果绝不会在未暂存的情况下混过去。
设置 piped: true 的原因是执行顺序。 Ruff 自己建议先运行带修复功能的 lint,再进行格式化,因为 --fix 生成的代码之后还需要重新格式化。启用 piped 也意味着先运行各项防护检查,发现泄露的密钥时就会在任何工具耗费时间之前阻止提交。
编辑此配置前,值得了解的注意事项:
- 当没有任何文件匹配时,
glob会静默跳过整个任务——这是门禁漏洞,而非便利功能。即使某个任务配置了glob,但run中没有{staged_files},它仍会受该 glob 过滤。因此,在只修改了pyproject.toml的提交中,配置了glob: "*.{py,pyi}"的ty会被跳过——而[tool.ty]和依赖版本固定信息恰好就在这个文件里。已验证:此类提交会显示ty (skip) no matching staged files,且完全不会执行类型检查。任何检查整个项目而非暂存文件的任务都不能配置 glob。上面的 ruff 任务保留了 glob,因为它们只针对{staged_files}操作。 **匹配一个或多个目录,而不是零个或多个。glob: "src/**/*.py"不会匹配src/main.py。使用glob_matcher: doublestar,即可获得其他工具都有的行为。- 配置文件的位置至关重要。 lefthook 只会自动发现仓库根目录或
.config/下的配置(从 v1.11.12 起支持后者)。放在其他位置时,提交会静默停止运行钩子,因为 git 会直接调用钩子,任何任务运行器配方都无法拦截。 - 绝不要在
pre-push中配置会修改内容的任务。 在那里重写文件的任务会导致推送失败,并留下未提交的修改。--fix应放在pre-commit中,由stage_fixed处理;pre-push应保持只读,像上面的pytest任务一样。 - 可以使用
file_types,以应对 glob 过于宽泛的情况——支持text、binary、executable、symlink以及包括text/x-python在内的 MIME 类型。 - 已删除的文件不会出现在
{staged_files}中,相应任务会因no files for inspection而跳过,因此只删除文件的提交不会报错。 - 钩子运行期间,未暂存的修改会被隐藏,并在运行后恢复,因此门禁检查的是你实际要提交的内容,而不是有未提交修改的工作区。已在 2.1.12 上验证:暂存一个干净文件,同时将有问题的副本留在未暂存状态,检查会通过,提交干净版本,并恢复未暂存的修改。
- lefthook 在安装前不会运行。
assert_lefthook_installed: true会将缺少二进制文件视为失败,避免钩子静默不运行。应将lefthook install纳入新成员上手流程。 lefthook validate会在 CI 中检查配置是否格式错误;lefthook dump会打印合并后的有效配置。被 git 忽略的lefthook-local.yml允许开发者添加或跳过任务,而不会强加给队友。
离开 pre-commit 后你会失去什么。 pre-commit-hooks 库没有 lefthook 的对应版本。其中有三个钩子值得保留,已在上面的 guards 组中手动实现:detect-private-key、check-merge-conflict 和 check-added-large-files。另外四个没有复刻:check-yaml、check-toml、end-of-file-fixer、trailing-whitespace 和 mixed-line-ending:ruff 的格式化工具已经会处理 Python 文件的空白和末尾换行,其余钩子则只用于非 Python 文件。如果你的仓库中有很多手动编辑的 YAML 文件,可以将它们作为 shell 任务加回来。你还会失去 pre-commit autoupdate 和托管服务 pre-commit.ci——作为交换,现在工具版本只需在一个地方更新:uv lock --upgrade。
项目结构
始终使用 src 布局:
my-project/
src/
my_project/
__init__.py
cli.py
models.py
tests/
conftest.py
test_models.py
pyproject.toml
Justfile
uv.lock
.python-version
lefthook.yml
.gitignore
日常工作流
just check # Type check + lint + format
just test # Run tests
just test -x # Stop on first failure
just fix # Auto-fix lint issues
uv add httpx # Add a dependency
uv add --dev hypothesis # Add dev dependency
uv sync # main deps + dev (dev is in default-groups)
uv sync --all-groups # everything in [dependency-groups]
uv run python -m my_project # Run the project
CI(GitHub Actions)
在 CI 中运行与 just check + just test 相同的检查。在 .github/workflows/ci.yml 中添加以下内容:
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/[email protected] # pin the full version - see below
- run: uv sync --all-groups
- run: uv run ty check
- run: uv run ruff check --output-format github
- run: uv run ruff format --check
- run: uv run pytest
- run: uv run lefthook validate
astral-sh/setup-uv 会安装 uv、管理 .python-version 中指定的 Python 安装,并缓存解析器。无需单独添加 setup-python 步骤。
关于该版本固定,需要注意两点:
- 固定完整版本,不要使用
@v10。 从 v8 开始,setup-uv 不再发布这两种浮动标签——“为进一步提高安全性,我们将停止发布次要版本标签。你将无法再使用@v8或@v8.0。” 已确认:v6和v7引用有效,v8/v9/v10则返回 404。只有精确到补丁版本的标签存在,因此请固定到其中一个版本或提交 SHA。 - 不要设置
enable-cache: true。 默认值为auto。从 v10 开始,该选项会在pull_request_target、workflow_run和release中刻意禁用缓存,以防止缓存投毒。强制启用会关闭这项保护。
迁移现有项目
# 1. Install uv if not present
brew install uv
# 2. Convert requirements.txt to pyproject.toml deps
uv add -r requirements.txt
# 3. Replace mypy with ty
uv remove --dev mypy
uv add --dev ty
# 4. Replace black/flake8/isort with ruff
uv remove --dev black flake8 isort
uv add --dev ruff
# 5. Replace pre-commit with lefthook
uv run pre-commit uninstall # while it can still read its own config
uv remove --dev pre-commit
rm .pre-commit-config.yaml
uv add --dev lefthook
uv run lefthook install
# 6. Apply pyproject.toml config sections from template above
# 7. Create Justfile and lefthook.yml from templates above
# 8. Run: just check
lefthook install 不会覆盖现有钩子,而是将其重命名为 .git/hooks/pre-commit.old 并告知你。先运行 pre-commit uninstall,可以免去删除遗留文件的步骤。
参考文档
references/ 中提供了各工具的详细指南:
- uv-reference.md - 项目初始化、依赖项、锁定/同步、Python 版本、构建/发布
- ty-reference.md - 配置、规则、CLI 标志、已知限制
- ruff-reference.md - 规则集、格式化选项、按文件忽略规则、CI 集成
- pytest-reference.md - 插件、夹具、异步测试、conftest 模式
- justfile-reference.md - 语法、变量、参数、shebang 配方、设置