mcpField
I
improving-python-code-quality

improving-python-code-quality

github.com/wdm0006测试与质量109175 次安装2 天前更新
代码仓库

关于 improving-python-code-quality

面向 Python 库和应用维护者的代码质量技能,涵盖 Ruff、mypy、类型提示、常见缺陷、显式错误处理及可复现性。

主要功能

  1. 01Ruff 检查与格式化指南
  2. 02mypy 配置和类型提示模式
  3. 03常见编码陷阱示例
  4. 04让失败显式可见的处理建议
  5. 05确定性与可复现性实践
  6. 06Python 项目组织与检查清单

适用场景

  1. 01配置代码检查与类型检查
  2. 02检查错误处理是否会静默失败
  3. 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.md
RUFF_CONFIG.md
TYPE_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 库开发指南中的代码质量部分。以下文章提供了更深入的讲解:

评论

支持 Markdown