从 JS/TS 到 Python:前端开发者的无缝迁移指南(三) 工程化篇
前两篇分别讲完了基础语法和服务端开发。这一篇聚焦到工程化——代码规范、类型检查、构建发布、CI/CD、文档体系。这些是让代码从”能跑”到”能维护”的关键基础设施。
对于习惯了 ESLint + Prettier + TypeScript + package.json 一条龙的前端开发者来说,Python 的工程化生态会给你一种”既熟悉又散装”的感觉——原理相通,但工具分散在不同维度,需要你主动拼装。
阅读前提:本文假设你已经读过前两篇,或至少熟悉 Python 基础语法、
pyproject.toml的基本概念、asyncio 和 FastAPI。
一、项目结构与目录约定
Node 项目的目录结构没有”官方标准”——src/ 还是根目录放源码、lib/ 还是 dist/ 放构建产物,全看团队偏好。Python 在这方面有更强的社区共识,但也分两类主流方案。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 源码目录 | src/ 或根目录,无强约定 |
src layout 或 flat layout |
Python 强烈推荐 src layout 避免导入陷阱 |
| 包标识 | package.json 即项目根 |
pyproject.toml 或 setup.py 所在目录 |
Python 多一个”可编辑安装”概念 |
| 模块即文件 | *.ts / *.js |
*.py |
Python 文件层次直接映射为导入路径 |
| 命名空间包 | npm workspace / scoped packages | 隐式命名空间包(PEP 420) | Python 不需要 package.json 来声明 workspace |
| 测试目录 | __tests__/、*.test.ts(co-located 或独立) |
tests/(几乎总是独立目录) |
Python 社区强烈倾向于测试代码与源码分离 |
1.1 src layout vs flat layout
Python 社区有两种主流项目布局。
Flat layout(扁平布局):
my_project/
pyproject.toml
my_package/
__init__.py
core.py
tests/
test_core.py
src layout(推荐):
my_project/
pyproject.toml
src/
my_package/
__init__.py
core.py
tests/
test_core.py
Flat layout 看起来更直接——源码就在项目根目录下。但强烈推荐 src layout,原因很实际:
// Node:这个问题在 Python 中同样存在
// 如果源码在根目录,import 的解析起点就是项目根
// 你可能会意外导入到"开发中的版本"而不是"安装后的版本"
# Python flat layout 的陷阱:
# 在项目根目录直接运行 python 时,当前目录默认在 sys.path 中
# 这会让你"意外地能导入",但安装后就可能找不到包
# src layout 强制你先 pip install -e .,从而更早暴露导入问题
避坑警告:如果你的项目里有
pyproject.toml+ 源码目录,但没做可编辑安装(pip install -e .),那么”为什么 import 不到自己的包”会是你遇到的第一类问题。srclayout 通过强制可编辑安装,让这个问题直接暴露,反而减少了隐蔽 bug。
安装时如何配置 pyproject.toml:
# pyproject.toml — src layout 的包发现配置
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
1.2 __init__.py 的工程角色
第一篇讲过 __init__.py 是”告诉 Python 这个目录是包”。在工程化层面,它的角色远不止于此。
// Node:index.ts 常用于"统一再导出"
// lib/index.ts
export { fetchUser } from "./user";
export { formatDate } from "./date";
// 使用者可以:
// import { fetchUser, formatDate } from "./lib";
# Python:__init__.py 承担同样的"对外整理出口"职责
# my_package/__init__.py
from .user import fetch_user
from .date import format_date
# 使用者可以:
# from my_package import fetch_user, format_date
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 目录级入口 | index.ts(约定,非强制) |
__init__.py(语言级机制) |
Python 的 __init__.py 有运行时语义 |
| 子模块再导出 | 常见 export * from(星导出) |
from .module import * 配合 __all__ |
Python 的 __all__ 明确控制星导入范围 |
| 包初始化 | 无语言级等价物 | __init__.py 顶层代码会在导入时执行 |
注意不要放进重副作用逻辑 |
| 延迟导入 | import() 动态导入 |
在 __init__.py 里按需 import |
Python 同样可以延迟加载子模块 |
__all__ 控制星导入:
# my_package/__init__.py
__all__ = ["fetch_user", "format_date"] # 只暴露这两个名字
from .user import fetch_user
from .date import format_date
from .internal import _helper # 不放进 __all__,星导入看不到
# 使用时:
from my_package import * # 只会导入 fetch_user 和 format_date
避坑警告:
__all__只影响from package import *,不影响显式导入。from my_package import _helper依然能导进来。Python 的”私有”靠_前缀约定而非语言强制——这和 JS 的#private不同。
1.3 包命名规范
Python 官方的包命名指南来自 PEP 8 和 PEP 423:
| 规范项 | JS/TS | Python |
|---|---|---|
| 包名格式 | 无统一标准(camelCase 或 kebab-case) |
全小写 + 下划线(snake_case) |
| 命名唯一性 | npm scope(@myorg/pkg) |
PyPI 全局命名空间,先到先得 |
| 包名与导入名 | 可以不同(lodash 导入 _) |
强烈建议包名 ≈ 导入名 |
| 顶层包名冲突 | npm scope 可解决 | 无 scope 机制,同名就是冲突 |
// Node:npm 的 scope 机制天然避免冲突
// import { foo } from "@myorg/utils";
// import { foo } from "@otherorg/utils"; // 同名包不冲突
# Python:PyPI 是全局扁平的,同名直接冲突
# 包名 my_utils 只能有一个发布者
# 所以 Python 包的命名往往更"具体化"
# pip install requests # ✅ 知名 HTTP 库
# pip install requests-toolbelt # ✅ 相关但不同的包名
1.4 pyproject.toml 深度解析
pyproject.toml 是 Python 项目的”中央配置文件”,对标 Node 的 package.json。但二者不是简单的 1:1 关系。
先看一个完整的对照结构:
// package.json — Node 项目配置文件
{
"name": "my-project",
"version": "1.0.0",
"description": "A sample package",
"scripts": { "dev": "tsx src/index.ts" },
"dependencies": { "express": "^4.18.0" },
"devDependencies": { "typescript": "^5.0" },
"engines": { "node": ">=18" }
}
# pyproject.toml — Python 项目配置文件
[project]
name = "my-project"
version = "1.0.0"
description = "A sample package"
requires-python = ">=3.10"
dependencies = [
"fastapi>=0.100.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.3.0",
]
[project.scripts]
my-cli = "my_package.cli:main" # 安装后生成命令行入口
两者的关键差异:
| 维度 | package.json |
pyproject.toml |
|---|---|---|
| 配置文件格式 | JSON | TOML |
| 脚本/命令 | npm scripts(内置) |
[project.scripts](命令行入口)或 [project.gui-scripts] |
| 依赖分组 | dependencies / devDependencies |
dependencies / optional-dependencies |
| 不区分 dev/prod | devDependencies 只开发时安装 |
optional-dependencies 按需安装,无内置 dev/prod 概念 |
| 元数据字段 | name、version、description 等 |
相同字段,但遵循 PEP 621 |
| 运行时要求 | engines.node |
requires-python |
| 入口点 | main / module / exports |
[project.scripts] 或 [project.entry-points] |
| 发布配置 | 单独的 publishConfig |
统一在 pyproject.toml 中 |
[project.scripts]:Python 的命令行入口
这是 package.json 中 "bin" 的直接对应物:
// package.json
{ "bin": { "my-cli": "./cli.js" } }
# pyproject.toml
[project.scripts]
my-cli = "my_package.cli:main"
# 格式:命令名 = "模块路径:函数名"
# 安装后,终端直接运行 `my-cli`
# my_package/cli.py 中只需要定义一个 main 函数
def main():
print("Hello from my-cli!")
避坑警告:
pyproject.toml不等于package.json。它只声明元数据和依赖,不负责运行脚本。Python 没有npm run dev的等价物——你需要 Makefile、task runner、或直接uv run。
1.5 命名空间包与 Monorepo 基础
Python 的命名空间包机制比 npm workspace 更轻量。npm 需要 package.json + workspace 配置声明包之间的关系;Python 的命名空间包完全是路径约定。
# npm workspace — 显式声明
my-monorepo/
package.json # { "workspaces": ["packages/*"] }
packages/
lib-a/
package.json # { "name": "@myorg/lib-a" }
lib-b/
package.json # { "name": "@myorg/lib-b" }
# Python 命名空间包 — 路径即结构(PEP 420)
my-monorepo/
pyproject.toml
packages/
myorg-lib-a/
src/
myorg/ # 同一个命名空间
lib_a/
__init__.py
myorg-lib-b/
src/
myorg/ # 同一个命名空间
lib_b/
__init__.py
关键:两个包的 myorg/ 目录都不需要 __init__.py——这就是 PEP 420 的隐式命名空间包。把两个包都安装后:
from myorg.lib_a import something
from myorg.lib_b import another
两边都能正常导入,就像它们来自同一个顶层包。
避坑警告:如果你想用命名空间包,不要在命名空间目录里放
__init__.py。放了就变成普通包,Python 不会去其他安装路径寻找同名包的子模块。
1.6 本节小结
这一节最值得记住的工程习惯:
- 新项目默认用
srclayout——pip install -e .做可编辑安装,提前暴露导入问题 __init__.py是你的对外出口整理文件——合理使用__all__控制公开 API- 包名全小写 + 下划线——不要带进 npm 的
kebab-case或camelCase习惯 pyproject.toml是中央配置但不是package.json的完整替代品——脚本执行靠 Makefile 或其他工具- 命名空间包不需要声明——路径结构对了就行
二、代码规范与自动格式化
Node 前端项目几乎标配 ESLint + Prettier。Python 这部分的工具链也在走向统一,而且速度更快——你不需要组合三个工具,两个就够。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 风格指南 | 社区惯例,无单一权威规范 | PEP 8 是事实标准 | Python 有官方风格指南且被广泛遵守 |
| 格式化工具 | Prettier | Black / Ruff format | Black 是”零配置、格式化即风格”的先驱 |
| Lint 工具 | ESLint | Ruff | Ruff 同时做格式化和 lint,且用 Rust 实现 |
| Import 排序 | eslint-plugin-import |
isort / Ruff | Python 的 import 排序有更明确的分类规则 |
| 配置入口 | .eslintrc.* / eslint.config.mjs |
pyproject.toml([tool.ruff] 等) |
Python 生态倾向于统一用 pyproject.toml |
| 自动修复 | eslint --fix |
ruff check --fix、ruff format |
语义相似 |
2.1 PEP 8:Python 社区的代码风格宪章
如果你只记住 PEP 8 的几条高频规则,就覆盖了 80% 的日常场景:
| 规则 | PEP 8 要求 | JS/TS 惯例对照 |
|---|---|---|
| 缩进 | 4 空格 | 2 空格(最常见) |
| 行长 | 最多 79 字符(文档/注释 72) | 80-120 字符(Prettier 默认 80) |
| 空行 | 顶层定义之间 2 空行,方法之间 1 空行 | 无强制约定 |
| import | 每个 import 独占一行,分组排序 | 无强制约定(靠 eslint-plugin-import) |
| 空格 | 运算符两侧各一个空格,逗号后一个空格 | 基本一致 |
| 命名 | snake_case 变量/函数,PascalCase 类 |
camelCase 变量/函数 |
| UTF-8 | 源码默认 UTF-8 | 源码默认 UTF-8 |
PEP 8 不是法律,但它减少了你和任意 Python 开发者之间的认知摩擦。真正重要的是工具自动执行,而不是逐条死记。
2.2 Black:Python 的 Prettier
Black 的哲学:格式化没有选项可调,风格不可配置。这比 Prettier 更激进——Prettier 好歹有几项可配置;Black 几乎只有 --line-length。
// Prettier: 少量可配置项
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"printWidth": 80
}
# pyproject.toml — Black 配置
[tool.black]
line-length = 88 # Black 的默认值(比 PEP 8 的 79 略大,故意的)
target-version = ["py310"]
为什么 Black 选择 88?作者的解释是:比 80 多 10% 的余地,显著减少换行次数,同时仍适合两个编辑器面板并排显示。
使用方式:
# 检查格式
$ black --check src/
# 自动格式化
$ black src/
# 等价于 Node:
# $ prettier --check src/
# $ prettier --write src/
2.3 Ruff:Python 的杀手级 Linter
Ruff 是近两年 Python 生态最大的单体工具突破。用 Rust 实现,同时替代了:
| JS/TS 工具 | Ruff 对应能力 |
|---|---|
| ESLint | ruff check(lint 规则) |
| Prettier | ruff format(格式化) |
| eslint-plugin-import | ruff check(import 排序规则 I) |
| eslint-plugin-unicorn | 大量内置规则(PL、SIM 等) |
| 各种插件拼装 | Ruff 内置 800+ 规则,覆盖 Flake8、pyflakes、isort、pydocstyle 等 |
推荐配置:
# pyproject.toml — Ruff 推荐配置
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort (import 排序)
"N", # pep8-naming
"UP", # pyupgrade(用新版语法)
"B", # flake8-bugbear(常见陷阱)
"SIM", # flake8-simplify(简化代码)
"C4", # flake8-comprehensions(推导式建议)
]
ignore = ["E501"] # 行长交给 Black/Ruff format 处理
[tool.ruff.format]
quote-style = "double" # "double" 或 "single"
使用方式:
# Lint 检查
$ ruff check src/
# Lint 自动修复
$ ruff check --fix src/
# 格式化
$ ruff format src/
# 等价于 Node:
# $ eslint src/
# $ eslint --fix src/
# $ prettier --write src/
避坑警告:Ruff 的
select规则前缀对应不同的工具来源。不加select时 Ruff 默认只启用E和F——远少于你预期的覆盖面。务必显式配置select,或者直接用select = ["ALL"]然后按需ignore。
Import 排序是 Ruff 的一个高频特性,对标 eslint-plugin-import 的 order 规则:
// JS: import 分为 builtin → external → internal
import fs from "node:fs"; // builtin
import express from "express"; // external
import { helper } from "./utils"; // internal
# Python: import 分为 stdlib → third-party → first-party
# Ruff 的 I 规则自动排序和组织:
import os # 标准库
import sys # (同一组内按字母序)
import fastapi # 第三方
import uvicorn # (同一组内按字母序)
from my_package import core # 本地/项目内
from my_package import utils
2.4 工程建议:统一用 Ruff 就够了
两年前你还需要 Black + isort + Flake8 + ... 拼装全家桶。现在一个工具就够了:
# 安装
$ uv add --dev ruff
# 日常使用
$ ruff check --fix src/ tests/ # lint + 自动修复
$ ruff format src/ tests/ # 格式化
对比 Node 的同类工作流:
# Node 需要至少两个工具
$ npx eslint --fix src/
$ npx prettier --write src/
# Python 一个就够了
$ ruff check --fix src/ && ruff format src/
如果你怀念 ESLint 的可配置性和 Prettier 的无脑格式化,Ruff 把两者很好地统一到了一个工具里。Ruff format ≈ Prettier(零配置优先),Ruff check ≈ ESLint(可配置的 lint 规则库)。
2.5 本节小结
- PEP 8 是 Python 的风格基线——用工具执行,不要靠记忆
- Ruff 一个工具替代 Black + isort + Flake8 + 各种插件——对标 ESLint + Prettier
- 务必显式配置
select——默认规则覆盖面太窄 ruff check --fix和ruff format是两个互补命令——前者修逻辑问题,后者修格式问题
三、静态类型检查
TypeScript 开发者最自然的恐惧是:”Python 没有编译时类型检查,代码安全吗?”答案是:Python 有静态类型检查,但它不是语言内置的——而是通过可选的第三方工具实现。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 类型系统 | TypeScript(编译时) | 类型标注 + mypy/pyright(可选运行时前检查) | Python 的类型检查是渐进式、可选的 |
| 类型擦除 | TS 编译后类型消失 | Python 运行时忽略类型标注 | Python 的类型标注默认只是注释 |
| 检查工具 | tsc --noEmit |
mypy / pyright / pylance |
Python 有多个竞争工具 |
| IDE 支持 | tsserver 统一 | Pyright(VS Code)/ PyCharm 内置 | Pyright 是 VS Code Python 体验的核心 |
| 严格模式 | strict: true |
strict = true(mypy)、typeCheckingMode: "strict"(pyright) |
两边思路惊人相似 |
| 类型文件 | .d.ts |
.pyi(stub 文件) |
相似的接口声明文件机制 |
3.1 Python 类型标注的本质
TS 类型在编译时就生效——没通过类型检查,代码不会产出 JS。Python 完全不同:
// TypeScript:类型错误 = 编译失败
const x: number = "hello"; // ❌ Type 'string' is not assignable to type 'number'
# Python:类型标注运行时完全不检查
x: int = "hello" # ✅ Python 解释器根本不管,运行正常
你需要在独立步骤中运行类型检查工具:
# 等价于 tsc --noEmit
$ mypy src/ # mypy
$ pyright src/ # pyright
3.2 mypy:Python 类型检查器的鼻祖
mypy 是最早的 Python 静态类型检查器,对标 tsc --noEmit:
// 定义一个带类型的函数
function greet(name: string): string {
return `Hello, ${name}`;
}
greet(123); // ❌ tsc: Argument of type 'number' is not assignable to parameter of type 'string'
# 同样的类型标注
def greet(name: str) -> str:
return f"Hello, {name}"
greet(123) # ✅ Python 解释器不报错
# 但 mypy 会报:error: Argument 1 to "greet" has incompatible type "int"; expected "str"
mypy 配置:
# pyproject.toml
[tool.mypy]
strict = true # 启用所有严格检查
python_version = "3.10"
warn_return_any = true # 返回 Any 时警告
warn_unused_configs = true # 检查配置是否被使用
disallow_untyped_defs = true # 禁止无类型标注的函数
disallow_incomplete_defs = true # 禁止部分标注的函数
[[tool.mypy.overrides]]
module = ["tests.*"]
ignore_errors = false # 测试文件也检查
避坑警告:
strict = true是 mypy 最有价值的模式,但它对存量和第三方库都要求类型标注。如果从无到有地给旧项目加 mypy,先从strict = false开始,逐步收紧规则。
3.3 Pyright:微软出品,VS Code 原生搭档
Pyright 是微软开发的类型检查器,也是 VS Code Pylance 的底层引擎。对标 TS 的 tsserver——它更偏向 IDE 体验和增量检查。
| 维度 | mypy | pyright |
|---|---|---|
| 实现 | Python(核心用 C 加速) | TypeScript(运行在 Node 上) |
| 速度 | 中等 | 快(增量检查极快) |
| 严格程度 | 可配置,默认相对宽松 | 可配置,默认更激进 |
| VS Code 集成 | 需要配置 | 零配置(Pylance 内置) |
| 第三方 stub | 需要 types-* 包 |
自动解析 + 需要 types-* 包 |
| 配置复杂度 | 中等 | 低 |
| 社区使用 | 历史最长,CI 中广泛使用 | 快速增长,IDE 体验更好 |
Pyright 配置(pyproject.toml):
[tool.pyright]
typeCheckingMode = "strict" # basic / standard / strict
pythonVersion = "3.10"
reportMissingTypeStubs = false # 对缺 stub 的第三方库降低警告
reportUnnecessaryTypeIgnoreComment = true # 提醒多余的 type: ignore
选型建议:IDE 里 Pyright/Pylance 完胜——你根本不需要额外配置就能在 VS Code 中获得类型提示和错误标记。CI 流程中 mypy 仍然更常见和稳定,但 Pyright 增长迅速。两者都配置也不冲突——像同时开了 tsc 和 eslint 的类型规则检查。
3.4 渐进式类型的迁移策略
这是 Python 类型系统最吸引人的特性——你可以逐步加类型,不需要一次全改。这和 TypeScript 的 strict: false → strict: true 迁移路径类似。
# 阶段一:完全无类型
def get_user(user_id):
user = db.query(user_id)
return {"name": user.name, "age": user.age}
# 阶段二:加上最关键的参数和返回值类型
def get_user(user_id: int) -> dict[str, str | int]:
user = db.query(user_id)
return {"name": user.name, "age": user.age}
# 阶段三:使用更精确的类型
from typing import TypedDict
class UserInfo(TypedDict):
name: str
age: int
def get_user(user_id: int) -> UserInfo:
user = db.query(user_id)
return {"name": user.name, "age": user.age}
迁移的工程顺序:
| 优先级 | 迁移内容 | 理由 |
|---|---|---|
| 1 | 公共 API 的函数签名 | 调用方最多,收益最大 |
| 2 | 数据结构的类型定义 | 数据流清晰后,bug 明显减少 |
| 3 | 内部工具函数 | 依赖关系越深,类型越有价值 |
| 4 | 测试文件 | 测试本身就是类型检查的补充 |
| 5 | 脚本和一次性代码 | 收益低,可以不标 |
3.5 Stub 文件:.d.ts 的 Python 对应物
TS 用 .d.ts 为 JS 库提供类型声明。Python 用 .pyi stub 文件做同样的事:
// DefinitelyTyped: node_modules/@types/express/index.d.ts
declare module "express" {
export function json(): RequestHandler;
}
# typeshed: types-requests/requests/__init__.pyi(简化示例)
def get(url: str, params: dict | None = None) -> Response: ...
def post(url: str, data: dict | None = None) -> Response: ...
安装第三方 stub 包:
# 给 requests 加类型
$ uv add --dev types-requests
# 等价于 Node:
# $ npm install -D @types/express
types-* 包来自 typeshed,是 Python 版的 DefinitelyTyped。
3.6 本节小结
- Python 的类型标注默认不检查——需要 mypy 或 pyright 作为独立步骤运行
- IDE 里 Pyright/Pylance 零配置——和 VS Code 的 TS 体验几乎一样
- CI 里跑 mypy 或 pyright——等价于
tsc --noEmit,防止类型错误的代码合入 - 渐进式迁移——先标公共 API,再标数据结构,最后覆盖内部代码
types-*=@types/*——安装第三方库的类型 stub
四、Python 包构建与发布
Node 生态里 npm publish 一条命令从构建到发布全搞定。Python 这边的流程历史包袱更重,但现在已基本收束到标准化路径。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 分发包格式 | .tgz(源码 + 构建产物) |
sdist (.tar.gz) + wheel (.whl) |
Python 严格区分源码分发包和构建后分发包 |
| 构建工具 | tsc / esbuild / rollup 等 |
build 前端 + setuptools / hatchling / flit 后端 |
Python 的 build frontend/backend 是两层概念 |
| 发布目标 | npm registry | PyPI (pypi.org) | 类似的概念 |
| 版本规范 | SemVer | PEP 440(基本兼容 SemVer) | Python 的版本号语法更精细 |
| 发布命令 | npm publish |
twine upload dist/* |
Python 的工具链更离散 |
| 发布令牌 | npm token 或 .npmrc |
~/.pypirc 或 TWINE_PASSWORD 环境变量 |
同样是 token 认证 |
4.1 sdist vs wheel:两种分发包
这是 Python 打包最核心的概念,Node 没有完全对应的区分:
| 格式 | 扩展名 | 内容 | Node 类比 |
|---|---|---|---|
| sdist(源码分发包) | .tar.gz |
源码 + pyproject.toml,安装时需要构建步骤 |
类似于发布源码到 npm(没有 build 步骤) |
| wheel(构建分发包) | .whl |
预构建好的安装包,可直接安装 | 类似于发布 dist/ 目录 |
# 构建两种分发包
$ python -m build
# 产物:
# dist/
# my_project-1.0.0.tar.gz # sdist
# my_project-1.0.0-py3-none-any.whl # wheel(纯 Python,跨平台)
其中 wheel 的文件名编码了兼容性信息:
my_project-1.0.0-py3-none-any.whl
^^^^^ ^^^ ^^^^ ^^^
版本 Python ABI 平台
版本 (none=纯Python)
py3:支持 Python 3 任意版本none:纯 Python 无 ABI 依赖any:跨平台
如果包含 C 扩展,wheel 会变成平台特定格式,例如 my_pkg-1.0.0-cp310-cp310-manylinux_x86_64.whl。
避坑警告:PyPI 上至少要上传 sdist + 一个 wheel。只传 sdist 会让使用者在安装时每次都要重新构建,可能导致构建工具版本不一致的问题。
4.2 build backend 选型
Python 的构建系统分为前端(用户直接调用的命令)和后端(实际执行构建的库):
| 构建后端 | 前端体验 | Node 类比 | 选型建议 |
|---|---|---|---|
| hatchling | 配置简单,pyproject.toml 原生支持 | esbuild(快、现代) | 推荐新项目默认用 |
| setuptools | 最老牌,生态最大 | webpack(老牌、生态大) | 存量项目最常见 |
| flit | 极致简化,纯 Python 包 | tsup(零配置优先) | 简单纯 Python 库 |
| poetry | 集成度高,包管理+构建 | npm(管理+构建一体) | 包管理和构建一起用 |
| uv | Rust 实现,极快 | pnpm(快、现代化) | 强烈推荐,正在迅速成为新标准 |
一个典型的 pyproject.toml 构建配置(hatchling):
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-project"
version = "1.0.0"
requires-python = ">=3.10"
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
选型建议:如果你用 uv 管理依赖,直接搭配 hatchling 作为构建后端——uv 专注于包管理,hatchling 专注于构建,各司其职。
4.3 发布到 PyPI
# 1. 构建
$ python -m build
# 2. 发布
$ twine upload dist/*
# 测试 PyPI 先试水:
$ twine upload -r testpypi dist/*
认证配置:
# ~/.pypirc
[pypi]
username = __token__
password = pypi-xxxxxxxx
[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-xxxxxxxx
# 或用环境变量(CI 友好)
$ export TWINE_USERNAME=__token__
$ export TWINE_PASSWORD=pypi-xxxxxxxx
$ twine upload dist/*
对比 Node 流程:
# Node
$ npm run build
$ npm publish
# Python
$ python -m build
$ twine upload dist/*
避坑警告:PyPI 上曾经可删除和覆盖已发布的包,现在已不再允许——发布出去就永远在那里。这和 npm 的
unpublish限制类似。测试用 Test PyPI。
4.4 版本号管理
Python 官方版本规范是 PEP 440:
| 版本形式 | 示例 | 是否允许 |
|---|---|---|
| 正式版 | 1.0.0 |
✅ |
| 预发布 | 1.0.0a1(alpha)、1.0.0b1(beta)、1.0.0rc1 |
✅ |
| 开发版 | 1.0.0.dev1 |
✅ |
| 后发布修复 | 1.0.0.post1 |
✅ |
| 语义化版本 | ^1.0.0(npm 范围) |
❌ Python 不用 ^ / ~ 范围语法 |
Python 依赖声明中不使用 ^1.0.0 这种范围语法——而是用 PEP 440 的比较运算符:
# pyproject.toml 中的依赖版本规范
dependencies = [
"fastapi>=0.100.0,<1.0.0", # 0.100.0 到 1.0.0(不含)
"uvicorn[standard]>=0.20.0", # 带 extras 的依赖
"httpx~=0.24.0", # ~= 兼容版本,≈ npm 的 ^0.24.0
]
~= 是最接近 npm ^ 的运算符——它锁定主版本号和第二个非零位置:
~= 1.4.2 → >=1.4.2, ==1.4.* # 兼容 1.4.x 系列
~= 1.4 → >=1.4, ==1.* # 兼容 1.x 系列
4.5 本节小结
- sdist + wheel 都要上传——sdist 给源码,wheel 给快速安装
- hatchling 是新项目的默认构建后端——配置简单、速度快
- twine 发布,不是
pip publish——Python 没有 npm 那样大一统的 CLI - 版本号用 PEP 440,不用 npm 的
^/~语法——~=最接近^ - PyPI 不可删除——先用 Test PyPI 验证
五、CI/CD 实践
Python 和 Node 的 CI/CD 结构在 GitHub Actions 层面惊人相似——差异主要在缓存策略和依赖安装步骤。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 包管理器缓存 | node_modules 或 npm cache |
pip cache / uv cache | Python 缓存体积更小 |
| 多版本测试 | node-version: [18, 20, 22] |
python-version: ["3.9", "3.10", "3.11", "3.12"] |
语法完全一致 |
| Lint 检查 | npm run lint |
ruff check src/ |
工具不同,流程一致 |
| 类型检查 | tsc --noEmit |
mypy src/ 或 pyright src/ |
语义对等 |
| 测试运行 | npm test(jest/vitest) |
pytest |
命令不同,流程一致 |
| 构建发布 | npm run build && npm publish |
python -m build && twine upload dist/* |
Python 多一个 twine 步骤 |
5.1 最小 CI 工作流
# .github/workflows/ci.yml — Node 项目
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test
# .github/workflows/ci.yml — Python 项目
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- run: uv sync
- run: ruff check src/ tests/
- run: mypy src/
- run: pytest -v
对比之下,关键差异只有这几个替换:
| Node CI 步骤 | Python CI 步骤 |
|---|---|
actions/setup-node |
astral-sh/setup-uv(推荐)或 actions/setup-python |
npm ci |
uv sync(或 pip install -e ".[dev]") |
npm run lint |
ruff check src/ tests/ |
tsc --noEmit |
mypy src/ |
npm test |
pytest -v |
5.2 缓存策略
Node 的 node_modules 动辄几百 MB,npm/yarn/pnpm 各有缓存目录。Python 的依赖体积通常更小——但缓存策略同样重要:
# actions/setup-python 自带的 pip 缓存
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip" # 自动缓存 pip 依赖
# uv 的缓存——astral-sh/setup-uv 自带 enable-cache: true
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true # 一键开启,无需额外配置
uv 的缓存比 pip 快一个数量级——这一点和 pnpm 对 npm 的体验提升类似。
5.3 发布工作流
# .github/workflows/publish.yml — Node
name: Publish
on:
push:
tags: ["v*"]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# .github/workflows/publish.yml — Python
name: Publish
on:
push:
tags: ["v*"]
jobs:
publish:
runs-on: ubuntu-latest
environment: release # GitHub Environment(推荐用于 PyPI)
permissions:
id-token: write # 用于 trusted publishing
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync --no-dev
- run: python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
# 无需手动传 token——GitHub OIDC trusted publishing
推荐 PyPI trusted publishing:从 GitHub Actions 发布时,不再需要手动管理 PyPI token。配置方法见 PyPI 文档。
5.4 本节小结
- CI 结构 Node 和 Python 几乎一致——checkout → install → lint → typecheck → test → (publish)
- 用
astral-sh/setup-uv替代actions/setup-python——更快、缓存更好、配置更少 - 发布用 PyPI trusted publishing——不用手动管理 token
六、文档体系
TypeScript 项目的文档通常是 JSDoc/TSDoc 注释 + 独立文档站(VitePress/Docusaurus)。Python 的习惯更”一体化”——docstring 本身就够被工具提取,生成 HTML 文档站几乎零配置。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 代码内文档 | JSDoc / TSDoc | docstring(Google / NumPy / Sphinx 风格) | Python 的 docstring 有更多工具链支持 |
| 文档生成器 | TypeDoc / JSDoc | Sphinx / MkDocs | Sphinx 是 Python 文档的老牌王者 |
| 自动 API 文档 | TypeDoc 自动提取 | Sphinx autodoc / MkDocs mkdocstrings | 理念一致 |
| 文档托管 | GitHub Pages / Vercel | Read the Docs / GitHub Pages | Read the Docs 是 Python 生态特色 |
| 示例代码测试 | 无标准等价物 | doctest(内联测试) | Python 提供语言级别的文档即测试 |
6.1 Docstring 风格
Python 社区有三种主流 docstring 风格:
Google 风格(推荐新项目):
def fetch_user(user_id: int, include_posts: bool = False) -> dict | None:
"""Fetch a user by their ID.
Args:
user_id: The unique identifier of the user.
include_posts: If True, also fetch the user's posts.
Returns:
A dict with user data, or None if the user doesn't exist.
Raises:
ValueError: If user_id is not positive.
"""
if user_id <= 0:
raise ValueError("user_id must be positive")
...
NumPy 风格(科学计算项目常用):
def fetch_user(user_id, include_posts=False):
"""Fetch a user by their ID.
Parameters
----------
user_id : int
The unique identifier of the user.
include_posts : bool, optional
If True, also fetch the user's posts. Default is False.
Returns
-------
dict or None
User data, or None if not found.
"""
...
Sphinx 风格(最老牌,rst 指令):
def fetch_user(user_id, include_posts=False):
"""Fetch a user by their ID.
:param user_id: The unique identifier of the user.
:type user_id: int
:param include_posts: If True, also fetch the user's posts.
:returns: User data as dict, or None.
:rtype: dict or None
"""
...
| 风格 | 可读性(原始源码) | 工具支持 | 推荐场景 |
|---|---|---|---|
| ⭐⭐⭐ 最好 | 所有工具都支持 | 新项目首选 | |
| NumPy | ⭐⭐ 好 | Sphinx + napoleon | 科学计算/数据项目 |
| Sphinx | ⭐ 一般 | Sphinx 原生 | 老项目遗留,不推荐新用 |
避坑警告:不管你选哪种风格,整个项目保持一致比风格本身更重要。Ruff 的
pydocstyle规则(D前缀)可以检查 docstring 是否缺失和格式是否一致。
6.2 MkDocs:最小配置生成文档站
MkDocs + Material for MkDocs 是 Python 文档站的最快路径。
# mkdocs.yml — 最小配置
site_name: My Project
theme:
name: material # Material 主题(最流行的文档主题)
plugins:
- search
- mkdocstrings: # 自动从 docstring 生成 API 文档
handlers:
python:
paths: [src]
options:
show_source: true
# 安装 + 预览
$ uv add --dev mkdocs mkdocs-material mkdocstrings[python]
$ mkdocs serve # 本地预览 http://localhost:8000
# 构建静态站
$ mkdocs build # → site/
如果用 Node 类比:
| Node | Python |
|---|---|
| VitePress | MkDocs + Material 主题 |
| TypeDoc 插件 | mkdocstrings 插件 |
vitepress dev |
mkdocs serve |
vitepress build |
mkdocs build |
| Vercel / Netlify | Read the Docs / GitHub Pages |
6.3 从 docstring 到 API 文档:mkdocstrings
Mkdocstrings 对标 TypeDoc——从代码注释自动生成 API 文档:
# API 参考
## User 模块
::: my_package.user
options:
members:
- fetch_user
- create_user
show_root_heading: true
构建后,mkdocstrings 会读取 my_package/user.py 中所有函数的 docstring 和类型标注,自动渲染出带参数表格、返回值说明、异常列表的 API 文档页面。
6.4 doctest:文档即测试
这是 Node 生态里没有直接对应物的特性。在 docstring 中写 >>> 开头的交互式示例,运行 python -m doctest 就能验证它们是否仍然正确:
def add(a: int, b: int) -> int:
"""Add two integers.
>>> add(1, 2)
3
>>> add(0, 0)
0
>>> add(-1, 1)
0
"""
return a + b
# 运行 doctest——自动执行所有 docstring 中的示例
$ python -m doctest -v src/my_package/core.py
# 输出每个测试的结果,失败的会明确报错
doctest 不是写核心测试的主力工具,但非常适合保证”文档中的示例代码不会过时”。
6.5 本节小结
- Google 风格 docstring 是新项目首选——源码可读性最好,所有工具都支持
- MkDocs + Material + mkdocstrings = Python 的 VitePress + TypeDoc
- doctest 让文档中的示例代码变成可执行测试——防止文档过时
七、Git 工作流增强
这一节只讲两个高频场景:pre-commit hooks 和约定式提交。
7.1 pre-commit hooks
pre-commit 是 Python 生态的 Git hooks 管理框架,对标 lint-staged + husky。
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.0
hooks:
- id: ruff # lint
- id: ruff-format # 格式化
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
args: [--strict]
# 安装 hooks
$ pre-commit install
# 此后每次 git commit 前自动运行检查
# 手动全量运行
$ pre-commit run --all-files
对比 Node 的 husky + lint-staged:
// Node: .lintstagedrc.json
{
"*.ts": ["eslint --fix", "prettier --write"]
}
# Python: pre-commit 天然支持只检查变更文件
# 不需要额外的 lint-staged 包
| JS/TS 工具链 | Python 工具链 |
|---|---|
| husky | pre-commit |
| lint-staged | pre-commit(内置) |
| commitlint | commitizen / 手动校验 |
7.2 约定式提交
Commitizen 在 Python 生态中也有对应方案:
# 安装
$ uv add --dev commitizen
# 交互式提交
$ cz commit
# ? Select the type of change: feat
# ? What is the scope?:
# ? Write a short description: add user authentication
# ? ...
# 输出: feat: add user authentication
# pyproject.toml
[tool.commitizen]
name = "cz_conventional_commits"
version = "1.0.0"
tag_format = "v$version"
7.3 本节小结
- pre-commit 比 husky + lint-staged 的组合更一体化——配置文件和工具都更简单
- commitizen 在 Python 中和 Node 版本功能一致——交互式生成规范提交
八、任务自动化
Python 没有 npm scripts。这一直是 Node 开发者吐槽最集中的差距。但替代方案并不复杂。
8.1 Makefile:最通用的脚本入口
# Makefile — 跨语言通用方案
.PHONY: dev test lint format build
dev:
uvicorn app.main:app --reload
test:
pytest -v --cov=src --cov-report=term-missing
lint:
ruff check src/ tests/
format:
ruff format src/ tests/
ruff check --fix src/ tests/
build:
python -m build
$ make dev # 启动开发服务器
$ make test # 运行测试
$ make lint # 代码检查
对比 package.json scripts:
// Node: npm scripts
{
"scripts": {
"dev": "tsx --watch src/index.ts",
"test": "vitest --coverage",
"lint": "eslint src/",
"format": "prettier --write src/"
}
}
Makefile 的缺点是 Windows 用户需要额外安装 make(或使用 just)。
8.2 Just:现代 Makefile
just 是一个 Rust 写的命令运行器,语法比 Make 更友好,且跨平台:
# justfile
dev:
uvicorn app.main:app --reload
test *args:
pytest -v {{args}}
lint:
ruff check src/ tests/
mypy src/
format:
ruff format src/ tests/
ruff check --fix src/ tests/
# 带参数的命令
test-one test_name:
pytest -v -k {{test_name}}
$ just dev
$ just test
$ just test-one test_fetch_user
8.3 uv run:Python 的 npx
uv 提供了 uv run,相当于 npx——在项目虚拟环境中直接运行命令:
# Node: npx 运行项目依赖中的可执行文件
$ npx jest --coverage
$ npx eslint src/
# Python: uv run 等价行为
$ uv run pytest --cov
$ uv run ruff check src/
这意味着如果你用 uv 管理项目,甚至不需要 Makefile 来记住虚拟环境路径:
# 这些命令在不同机器上都能以相同方式运行——uv run 自动处理环境
$ uv run pytest
$ uv run ruff check src/
$ uv run mypy src/
推荐方案:
uv run解决”激活 venv”的路径问题,Makefile 或 justfile 定义标准化命令入口。两者结合,最接近npm scripts的体验。
8.4 本节小结
- Python 没有 npm scripts——用 Makefile、justfile 或
uv run替代 uv run≈npx——自动使用项目虚拟环境- Makefile +
uv run是最稳妥的跨环境组合——Makefile 定义命令,uv run管理环境
九、总结:Python 工程化全景对应图
| 工程化维度 | JS/TS 工具 | Python 工具 | 关键差异 |
|---|---|---|---|
| 项目配置文件 | package.json |
pyproject.toml |
TOML vs JSON,Python 无内置 scripts |
| 包管理器 | npm / yarn / pnpm | pip / uv | 推荐 uv,对标 pnpm 速度 |
| 虚拟环境 | node_modules(自动) |
venv(手动创建激活) |
Python 必须手动管理环境隔离 |
| 代码格式化 | Prettier | Ruff format(或 Black) | Ruff 一个工具同时做格式化和 lint |
| Linting | ESLint | Ruff check | Ruff 更快,内置规则更多 |
| Import 排序 | eslint-plugin-import | Ruff(I 规则)/ isort |
Ruff 的 I 规则内置实现 |
| 类型检查 | tsc –noEmit | mypy / pyright | Python 类型检查是可选的独立步骤 |
| 测试 | Jest / Vitest | pytest | pytest fixture 是杀手特性 |
| 构建 | tsc / esbuild / rollup | build + hatchling / uv | 构建前后端分离 |
| 发布 | npm publish | build + twine | Python 构建和发布分离为两步 |
| Git hooks | husky + lint-staged | pre-commit | pre-commit 一个工具搞定 |
| 文档生成 | TypeDoc / VitePress | MkDocs / Sphinx | MkDocs + Material 体验最接近 VitePress |
| 脚本执行 | npm run xxx | Makefile / just / uv run |
最需要适应的差距 |
| CI 安装 | npm ci |
uv sync |
结构完全一致 |
最后:迁移路径建议
如果你刚从前端转到 Python 工程化,推荐的搭建顺序:
uv init+srclayout——项目骨架- Ruff(lint + format)——代码规范
- mypy 或 pyright——类型检查
- pre-commit——自动化检查门禁
- pytest——测试框架
- GitHub Actions CI——持续的 lint + typecheck + test
- MkDocs——文档站
- build + twine——发布到 PyPI
全文完。