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. 01Pythonライブラリの公開インターフェース設計
  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?"
)
失敗は明確に、黙って済ませない

最もコストのかかるバグは、失敗と成功の区別がつかないものです。エラーが返ってこなければ、呼び出し側はすべてうまくいったと思い込みます。以下のパターンはいずれも、リリース済みのコードで実際の失敗を、気づかれない誤った結果に変えてしまいました。すべて防いでください。

失敗時に成功を示す値を書き込まないでください。 成功時と同じ状態を設定する 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"(さらに悪い場合は空の結果)を返すと、解析エラー、ファイルの欠落、バグがすべて同じ不透明な文字列にまとめられ、デバッグできなくなります。また、「問題は見つかりませんでした」と誤解されることも少なくありません。処理できる具体的な例外だけを捕捉し、それ以外は伝播させてください。

空または不完全な結果は、エラーを示すものではありません。 []、空の DataFrame、あるいは「接続が切れる前に取得したデータ」を返すと、有効な回答に見えてしまいます。ストリームの途中で RequestError が発生してもページの一部を返し、それを完全な結果として集計するページネーションでは、気づかれないまま誤った分析結果が生まれます。例外を発生させるか、呼び出し側が必ず確認しなければならない明示的な「不完全」マーカーを返してください。切り詰められた結果を、完全な結果のように見せてはいけません。

外部処理の回数は、実際に処理が行われる場所で計測してください。 list_all_items() の呼び出し全体でクォータやリクエスト予算を一度だけ消費すると、そのヘルパーが内部でページネーションを行うたびに過少計上になります。ひとつの予算単位で、HTTP リクエストが 10 回隠れてしまうこともあります。共有される最も低いリクエスト境界で予算を消費し、各ページ、再試行、詳細の取得をすべて計上してください。

# 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 呼び出しより上の位置に予算処理を置いてください。再試行のたびにプロバイダーのクォータを消費する場合は、再試行ロジックより下の位置に置いてください。複数ページを返す偽のページネーション対応トランスポートでテストし、結果の値と予算を消費した正確な回数の両方を検証してください。1 ページしか返さない偽物では、この契約を証明できません。

予期しない入力に対する何もしない処理は、気づかれないデータ破損につながります。 型の異なる列をスキップしたり、認識できないキーを無視したり、解析できないファイルを 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 で遅れて検証エラーを発生させ、オブジェクトが変更されていないことを検証します。例外の型だけをテストしても、このバグによる損害の半分は見逃してしまいます。

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 に「何か表示するもの」を用意するために)、作り出した数値を実データとして見せてしまうことがあります。失敗をそのまま示してください。もっともらしい嘘より、目に見えるエラーのほうが適切です。

終了コードが 0 以外でも、本当の出力を破棄しないでください。 returncode != 0 の場合に f"Error: {stderr}" を返すサブプロセスのラッパーでは、意図的に 0 以外で終了し、標準出力に結果を書き込むツールの回答が失われます。正常に実行できているのに、空の "Error: " として報告されてしまいます。失敗と判断する前に、標準出力と実際の終了時の仕様を確認してください。

実装モジュールだけでなく、公開インポートパスをテストする

パッケージが同じ名前の関数を2つ公開していても、動作するのは片方だけということがあります。

# 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"]

そのうえで、ユーザーに案内しているインポートパスを通じてテストします。同一性のアサーションは手軽な乖離防止策になり、動作のアサーションを1つ加えることで、公開経路から必要な初期化が行われることを確認できます。

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 スキルで管理しています。詳しくは ../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 スキルで扱っています。

詳しいパターンについては、以下を参照してください。

  • PATTERNS.md - ビルダー、ファクトリー、高度なパターン
  • EVOLUTION.md - API のバージョニングと移行ガイド

レビューチェックリスト

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

詳細情報

このスキルは、Will McGinnis による Guide to Developing High-Quality Python Libraries のエルゴノミクスのセクションを基にしています。さらに詳しくは、以下の記事を参照してください。

コメント