关于 improving-python-code-quality
面向 Python 库和应用维护者的代码质量技能,涵盖 Ruff、mypy、类型提示、常见缺陷、显式错误处理及可复现性。
主要功能
- 01Ruff 检查与格式化指南
- 02mypy 配置和类型提示模式
- 03常见编码陷阱示例
- 04让失败显式可见的处理建议
- 05确定性与可复现性实践
- 06Python 项目组织与检查清单
适用场景
- 01配置代码检查与类型检查
- 02检查错误处理是否会静默失败
- 03让生成结果和测试可复现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wdm0006/ python-skills --skill improving-python-code-quality安装后在 Claude Code 中输入/improving-python-code-quality确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
MYPY_CONFIG.mdRUFF_CONFIG.mdTYPE_PATTERNS.md详细介绍
Python 代码质量
快速参考
| 工具 | 用途 | 命令 |
|---|---|---|
| ruff | 代码检查 + 格式化 | ruff check src && ruff format src |
| mypy | 类型检查 | mypy src |
Ruff 配置
pyproject.toml 中的最简配置:
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.lint]
select = ["E", "W", "F", "I", "B", "C4", "UP"]
完整配置选项请参阅 RUFF_CONFIG.md。
MyPy 配置
[tool.mypy]
python_version = "3.10"
disallow_untyped_defs = true
warn_return_any = true
严格设置和覆盖项请参阅 MYPY_CONFIG.md。
类型提示模式
# Basic
def process(items: list[str]) -> dict[str, int]: ...
# Optional
def fetch(url: str, timeout: int | None = None) -> bytes: ...
# Callable
def apply(func: Callable[[int], str], value: int) -> str: ...
# Generic
T = TypeVar("T")
def first(items: Sequence[T]) -> T | None: ...
协议和高级模式请参阅 TYPE_PATTERNS.md。
常见反模式
# Bad: Mutable default
def process(items: list = []): # Bug!
...
# Good: None default
def process(items: list | None = None):
items = items or []
...
# Bad: Bare except
try:
...
except:
pass
# Good: Specific exception
try:
...
except ValueError as e:
logger.error(e)
# Bad: truthiness guard swallows a legitimate 0 / 0.0 / "" / False
hour = config.get("start_hour") or 9 # a valid 0 (midnight) silently becomes 9
if self.max_drawdown: # max_drawdown=0 silently disables the limit
enforce(self.max_drawdown)
# Good: guard on None, not on truthiness
hour = config.get("start_hour")
hour = 9 if hour is None else hour
if self.max_drawdown is not None:
enforce(self.max_drawdown)
只有当没有任何假值是有意义的输入时,x = x or default 才合适——也就是说,取值范围是 None 或对象,或者唯一的假值情况是空容器(例如 items = items or [])。对于 0、False 或 "" 等有意义的数值、布尔值或字符串字段,这会导致错误——请使用 x if x is not None else default。
# Bad: identity comparison against a literal (ruff flags this as F632)
if name is not "": # CPython interning makes it *sometimes* work — never rely on it
# Good: value comparison
if name != "":
启用的规则不一定会触发
代码检查工具会内置豁免机制,其中代价最高的那些往往难以察觉:规则已列入 select,任务也顺利通过,而你以为受到检查的那类错误却畅通无阻。在依赖某项检查来保护某些内容之前,先故意写一次违规代码,并确认它会被报告。
Ruff 的默认设置中影响最大的是 dummy-variable-rgx。它旨在让 _ 前缀的临时变量不触发未使用变量检查,但默认模式会匹配任何以下划线开头的名称——因此,F811(未使用名称的重新定义)会忽略代码库中的所有私有辅助函数。
# module.py — two module-level definitions. The FIRST one is dead code.
def _is_rate_limited(response): # never called; editing it has no effect
return response.status_code == 403
def _is_rate_limited(response): # this is the one that wins
return response.status_code == 403 and "rate limit" in response.text.lower()
对该文件运行 ruff check --select F811,检查会顺利通过。将两处名称都改为 is_rate_limited 后,规则会立即触发(已在 ruff 0.16 上验证)。检查机制正常工作——是这个名称选择了不受检查。
这种情况最常见于两个分支分别添加了相同的模块级辅助函数,而冲突解决时保留了两边的代码。没有任何检查变红;起初函数体通常一致;之后有人修改了无效副本,却想不明白为什么行为没有变化。
最好在配置中修正,让检查真正覆盖私有名称:
[tool.ruff.lint]
dummy-variable-rgx = "^_$" # only a bare `_` is a throwaway
采用这种设置前,请如实说明其中的权衡:_, keep = pair() 仍不会触发检查,但 _unused = compute() 现在会触发 F841。在经常使用 _name 形式的临时变量的代码库中,这确实会造成干扰——请删除赋值,或改用裸 _,而不是恢复原来的正则表达式。
此外,在解决了某个模块中两个分支都修改过的冲突后,请直接检查是否存在重复的模块级定义——这样可以捕获代码检查工具配置无法发现的名称遮蔽问题。这里特意使用了带锚点的模式:类或函数内部嵌套的 def/class 都有缩进,不会匹配;而意外遮蔽真正造成问题的正是这个作用域。
grep -oE '^(def|class) [A-Za-z_][A-Za-z0-9_]*' module.py | sort | uniq -d
这种习惯也适用于 Ruff 之外的检查:当某项检查至关重要时,故意引入一次错误并观察它被捕获,就像回归测试只有在你亲眼看到它变红后才值得信赖。
失败要明确:不要静默降级
代价最高的 Bug 不是崩溃,而是看起来像成功的失败。吞掉错误并返回看似合理的内容,会在下游破坏数据,却不发出任何出错信号。宁可抛出异常,或至少明确报告错误,也不要悄悄回退。
不要将异常压缩成通用字符串,也不要丢弃输出。
# Bad: real parse error becomes a one-line string; traceback and context lost
try:
return parse(path)
except Exception as e:
return {"error": str(e)} # every distinct failure looks the same
# Bad: a nonzero exit discards stdout — where many CLIs write their real summary
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
return f"Error: {result.stderr}" # tools that exit nonzero *by design*
# (e.g. "N findings") report empty here
# Good: let it raise (or return stdout+stderr+code so the caller can decide)
return {"stdout": result.stdout, "stderr": result.stderr, "code": result.returncode}
为成功和失败设置不同的标记值。 如果错误路径写入的状态值与成功路径相同,失败的运行就会被误认为已完成。
# Bad: on failure, status is set to the SAME value that means "done"
try:
run_job()
status = "READY"
except Exception:
status = "READY" # failures are now indistinguishable from success
# Good: a terminal error state the UI/caller can branch on
status = "ERROR"
失败时绝不要用伪造的数据替代。 获取失败时返回随机数据或示例数据,会让 UI 显示看似合理、实则虚构的数字。
# Bad: a network hiccup silently becomes made-up numbers
try:
return fetch_metrics()
except RequestError:
return generate_sample_data() # user can't tell real from fake
# Good: propagate the failure (or return an explicit sentinel the UI renders as an error)
明确标示部分结果,不要将其作为完整结果返回。 分页获取中途终止后返回已有数据,看起来与完整结果毫无区别。
# Bad: caller can't distinguish "12 items" from "12 of 900 before the API died"
def fetch_all():
items = []
for page in paginate():
try:
items.extend(page)
except RequestError:
break # silent truncation
return items
# Good: return completeness alongside the data (or raise)
return items, complete # caller warns loudly when complete is False
批处理循环:收集每项错误,不要只用 continue 跳过。 静默跳过错误输入,就无法知道覆盖范围是否不完整;而且,当部分操作报告错误、其他操作却不报告时,同批的其他操作会表现不一致。
# Bad: files that fail to load vanish with no trace
for f in files:
try:
process(load(f))
except Exception:
continue # how many were skipped? which ones?
# Good: accumulate skips and return them so callers (and automation) can see them
errors = []
for f in files:
try:
process(load(f))
except Exception as e:
errors.append({"file": f, "error": str(e)})
return {"processed": ..., "errors": errors}
核心原则:捕获错误时,要么进行有意义的恢复,要么让失败显而易见(抛出异常、以错误级别记录日志,或返回可区分的标记值)。在 except 中使用 return/continue/回退逻辑并生成看似正常的输出,正是静默损坏的根源。
确定性与可复现性
非确定性是一种隐性负担:频繁变动、难以审查的差异;不稳定的测试;以及无法根据种子复现的模拟。以下三种情况反复出现。
从无序容器序列化。 通过遍历 set 构建列表或 JSON 负载(或合并到 dict 后再遍历),会导致结果顺序每次运行都不同——str 哈希值在每个进程中都是随机的,因此重新生成的文件虽然数据相同,顺序却会变化;在一个项目的核心价值可能就是整洁差异的仓库中,真正的改动也会淹没在噪声里。序列化前先按总序排序。
# Bad: set iteration order isn't stable across runs
tags = [render(t) for t in tag_set]
json.dump(record, f) # nested sets/merges churn the output
# Good: sort before writing
tags = [render(t) for t in sorted(tag_set)]
json.dump(record, f, sort_keys=True)
使用多个 RNG,却无法注入。 同时从 random 和 numpy.random 取值的库,要想实现可复现,就必须为两者都设置种子——只为其中一个设置种子,另一个仍会自由生成,因此运行结果只有一半是确定的。而且,为全局 RNG(random.seed、np.random.seed)设置种子会破坏调用方的全局状态。接收种子(或 RNG 实例),并传递局部生成器,让调用方获得可复现性,而你无需访问其全局状态。
# Bad: two independent global RNGs, no way to seed from the outside
def simulate():
x = random.random() # stdlib global stream
y = np.random.normal() # numpy global — a *separate* stream
# Good: caller-supplied, local generators; one seed reproduces the whole run
def simulate(seed: int | None = None):
rng = random.Random(seed)
nprng = np.random.default_rng(seed)
x = rng.random()
y = nprng.normal()
解析上下文未固定。 依赖区域设置的解析(例如使用 %b/%a(月份/星期名称)的 datetime.strptime)会在不同机器上悄然改变行为。固定格式和区域设置,这样在你的机器上通过的测试在 CI 中也能通过。
非确定性也会削弱测试:会对输出执行 shuffle 的函数,会迫使断言写得非常宽松(“包含这些词中的任意一个”),导致测试无法捕获回归。让这个边界具有确定性——注入 RNG——并精确断言输出。
Python 风格习惯用法
# Iteration
for item in items: # Not: for i in range(len(items))
for i, item in enumerate(items): # When index needed
# Dictionary access
value = d.get(key, default) # Not: if key in d: value = d[key]
# Context managers
with open(path) as f: # Not: f = open(path); try: finally: f.close()
# Comprehensions (simple only)
squares = [x**2 for x in numbers]
模块组织
src/my_library/
├── __init__.py # Public API exports
├── _internal.py # Private (underscore prefix)
├── exceptions.py # Custom exceptions
├── types.py # Type definitions
└── py.typed # Type hint marker
检查清单
Code Quality:
- [ ] ruff check passes
- [ ] mypy passes (strict mode)
- [ ] Public API has type hints
- [ ] Public API has docstrings
- [ ] No mutable default arguments
- [ ] Specific exception handling
- [ ] Failures fail loud — no fabricated fallbacks, colliding success/error sentinels, or silently-truncated results
- [ ] Batch loops collect per-item errors instead of a bare `continue`
- [ ] Truthiness guards don't swallow valid 0/False/"" (guard on `is None`)
- [ ] No `is`/`is not` against literals (use ==/!=)
- [ ] Load-bearing lint rules verified to fire (F811 skips `_`-prefixed names under the default `dummy-variable-rgx`)
- [ ] No duplicate module-level `def`/`class` names after a conflict resolution
- [ ] Deterministic output: sort before serializing; seed/inject all RNGs (both `random` and `numpy`)
- [ ] py.typed marker present
了解更多
本文内容基于 Will McGinnis 编写的高质量 Python 库开发指南中的代码质量部分。以下文章提供了更深入的讲解: