mcpField
D
designing-python-apis

designing-python-apis

github.com/wdm0006开发109138 次安装2 天前更新
代码仓库

关于 designing-python-apis

designing-python-apis 是帮助开发者设计 Python 库接口的技能,涵盖命名、错误处理、验证、公开导入路径和测试实践。内容强调清晰呈现失败,并让 API 行为可预测。

主要功能

  1. 01API 简洁性与一致性原则
  2. 02渐进式信息呈现模式
  3. 03命名与参数设计建议
  4. 04明确错误处理实践
  5. 05变更状态前先验证
  6. 06测试公开导入路径

适用场景

  1. 01规划 Python 库的公开接口
  2. 02检查错误与输入处理
  3. 03测试文档中的包导入方式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wdm0006/python-skills --skill designing-python-apis

安装后在 Claude Code 中输入/designing-python-apis确认已加载。

附带文件EVOLUTION.mdPATTERNS.md

详细介绍

Python API 设计

核心原则

  1. 简洁性:让简单的事情保持简单,让复杂的事情成为可能
  2. 一致性:相似的操作以相似的方式运行
  3. 最小意外原则:按照用户的预期运行
  4. 可发现性:通过自动补全和帮助信息查找

渐进式披露模式

# Level 1: Simple functions
from mylib import encode, decode
result = encode(37.7749, -122.4194)

# Level 2: Configurable classes
from mylib import Encoder
encoder = Encoder(precision=15)

# Level 3: Low-level access
from mylib.internals import BitEncoder

命名约定

# Actions: verbs
encode(), decode(), validate()

# Retrieval: get_*
get_user(), get_config()

# Boolean: is_*, has_*, can_*
is_valid(), has_permission()

# Conversion: to_*, from_*
to_dict(), from_json()

错误处理

class MyLibError(Exception):
    """Base exception with helpful messages."""
    def __init__(self, message: str, *, hint: str = None):
        super().__init__(message)
        self.hint = hint

# Usage
raise ValidationError(
    f"Latitude must be -90 to 90, got {lat}",
    hint="Did you swap latitude and longitude?"
)
失败要明确报错,不能悄无声息

代价最高的 bug,是失败与成功无法区分。调用方没有收到错误,就会认为一切正常。这些模式都曾在已发布的代码中把真实失败变成悄无声息的错误答案——请逐一防范。

失败路径中不要写入成功标记。 except 代码块若设置了与成功运行相同的状态,就会让失败的任务看起来已经完成。

# BAD — any failure is reported to the user as a finished result.
try:
    result = run_job()
    status = "READY"
except Exception:
    status = "READY"        # failure now looks identical to success

# GOOD — distinct terminal states; the UI/caller can react.
try:
    result = run_job()
    status = "READY"
except Exception:
    log.exception("job failed")
    status = "ERROR"

不要把不同的失败都吞掉,只返回一条通用消息。 宽泛的 except Exception 若返回 "error: something went wrong"(更糟的是返回空结果),就会把解析错误、文件缺失和代码 bug 混成同一个不透明字符串,既难以调试,也常被误认为「未发现问题」。捕获你能处理的特定异常;其余异常应继续传播。

空结果或部分结果不是错误信号。 返回 []、空的 DataFrame,或「连接中断前已获取的内容」,看起来都像有效答案。如果分页过程中发生 RequestError,却仍返回部分页面,随后又将其当作完整结果汇总,就会产生悄无声息的错误分析。要么抛出异常,要么返回调用方必须检查的明确「未完成」标记——绝不能让截断结果冒充完整数据集。

在外部操作实际发生的位置计量。 如果配额或请求预算是在 list_all_items() 外层只扣一次,而该辅助函数内部会分页,那么预算就会少计:一个预算单位可能掩盖十次 HTTP 请求。应在共享的最低层请求边界扣除预算,这样每一页、每次重试和每次详情获取都会计入。

# BAD — the helper may issue an unbounded number of requests.
budget.spend()
items = client.list_all_items()

# GOOD — pagination cannot bypass the meter.
def request(method, url, *, budget, **kwargs):
    budget.spend()
    return http.request(method, url, **kwargs)

如果缓存命中不应计费,就将预算扣减放在原始 HTTP 调用上方;如果每次重试都会消耗服务提供方配额,就将其放在重试逻辑下方。使用会返回多个页面的模拟分页传输进行测试,并同时断言结果值和预算扣减的确切次数;只有一页的模拟对象无法验证这一约定。

遇到意外输入时不执行任何操作,属于悄无声息的数据损坏。 如果代码跳过类型不符的列、忽略无法识别的键,或对无法解析的文件执行 continue,却不报错也不说明情况,就会让调用方误以为操作已生效。如果无法处理某项输入,就应说明原因(抛出异常、发出警告,或返回逐项错误列表);不要悄悄置之不理。

验证错误不能留下部分变更。 inplace=True 的 API 若先重命名调用方拥有的列,再验证另一个参数,就可能在抛出正确异常的同时,破坏调用方后续的操作。请在首次变更前验证所有参数和前置条件。如果无法预先完成验证,就在副本上暂存操作,成功后再提交。

# BAD — invalid max_fraction still changes the caller's dataframe.
def detect(data, *, max_fraction=0.1, inplace=True):
    target = data if inplace else data.copy()
    target.rename(columns={"time": "timestamp"}, inplace=True)
    if not 0 < max_fraction < 0.5:
        raise ValueError("max_fraction must be between 0 and 0.5")
    return analyze(target)

# GOOD — the error path is side-effect free.
def detect(data, *, max_fraction=0.1, inplace=True):
    if not 0 < max_fraction < 0.5:
        raise ValueError("max_fraction must be between 0 and 0.5")
    target = data if inplace else data.copy()
    target.rename(columns={"time": "timestamp"}, inplace=True)
    return analyze(target)

通过测试明确约定:先快照调用方拥有的状态,再使用 inplace=True 触发较晚发生的验证错误,并断言对象保持不变。只测试异常类型,会漏掉这个 bug 会造成的破坏。

before = frame.copy(deep=True)
with pytest.raises(ValueError, match="max_fraction"):
    detect(frame, max_fraction=0.5, inplace=True)
pd.testing.assert_frame_equal(frame, before)

筛选后变为空,绝不能被解读为「一切正常」。 缩小规则集、检查集或工作列表的范围时,如果筛选结果为空,而代码走了「评估全部 → 0 个问题」的路径,就会在实际什么都没检查的情况下报告满分。请显式处理空结果:

selected = [r for r in rules if r.category in requested]
if not selected:                       # empty filter ≠ everything passed
    raise ValueError(f"no rules match {requested!r}")

绝不要伪造看起来真实的兜底结果。 获取失败时用示例数据或随机数据代替(只为让 UI「有内容可显示」),会把虚构数字当成真实数据呈现出来。应明确显示失败;看得见的错误胜过貌似合理的谎言。

退出码非零时,不要丢弃真实输出。 子进程封装器若在 returncode != 0 时返回 f"Error: {stderr}",就会丢失那些按设计以非零状态退出、并将结果写入 stdout 的工具所提供的答案——把成功运行报告成空的 "Error: "。在判定失败前,请先检查 stdout 和实际的退出语义。

测试公开导入路径,而不只是实现模块

一个包可以公开两个同名函数,但只有其中一个能正常工作:

# mylib/__init__.py
def initialize_parser() -> None:
    pass                         # stale placeholder

# mylib/parser.py
def initialize_parser() -> None:
    global _parser
    _parser = build_parser()     # real implementation

导入 mylib.parser.initialize_parser 的内部测试全部通过,但文档中所写的 from mylib import initialize_parser 调用却不起作用。尽管这两个定义各自都有效,这仍然是一次 API 破坏。

应重新导出规范对象,而不是对它进行包装或复制:

# mylib/__init__.py
from .parser import initialize_parser

__all__ = ["initialize_parser"]

然后,通过告知用户使用的路径进行测试。身份断言是一种成本低廉的防漂移措施,而一条行为断言则可以证明公开路径会执行所需的初始化:

import mylib
from mylib import parser

def test_public_initializer_is_canonical():
    assert mylib.initialize_parser is parser.initialize_parser
    mylib.initialize_parser()
    assert parser.is_initialized()

对便捷导出、兼容性适配器、控制台入口目标和文档中的导入示例,都要执行行为检查。只有当公开名称本来就应是直接别名时,才使用身份检查。只测试叶子模块只能证明实现正确,却无法说明公开 API 是否能调用到它。

弃用

以平稳的方式弃用:现在发出警告,注明移除版本,并且只在主版本发布时移除。警告机制(包括弃用参数、类和模块)以及迁移指南模板由 managing-python-releases skill 负责 — 参见 ../release-management/MIGRATION.md。

反模式

# Bad: Boolean trap
process(data, True, False, True)

# Good: Keyword arguments
process(data, validate=True, cache=False)

可变默认参数陷阱(def f(x: list = []))由 improving-python-code-quality skill 介绍。

详细模式请参阅:

审查清单

Naming:
- [ ] Clear, self-documenting names
- [ ] Consistent patterns throughout
- [ ] Boolean params read naturally

Parameters:
- [ ] Minimal required parameters
- [ ] Sensible defaults
- [ ] Keyword-only after positional clarity

Exports:
- [ ] Direct package exports reference the canonical implementation, not a stale stub
- [ ] Tests import through the documented public path and assert real behavior

Errors:
- [ ] Custom exceptions with context
- [ ] Helpful error messages
- [ ] Documented in docstrings
- [ ] Failures fail loud — no success sentinel on the error path
- [ ] Empty/partial results never masquerade as a complete answer
- [ ] No silent no-ops or fabricated fallback data
- [ ] Validation failures leave caller-owned inputs unchanged

了解更多

此 skill 基于 Will McGinnis 撰写的 《高质量 Python 库开发指南》中的 “易用性”部分。以下文章提供了更深入的介绍:

评论

支持 Markdown