AIにPythonを理解したコードを書かせる試行錯誤

Event:

AIを“相棒”にするための勉強会

Presented:

2025/10/29 nikkie

お前、誰よ

../_images/uzabase-white-logo.png

宣伝:書籍『サム・アルトマン』、いかがですか?

../_images/sama-book.jpg

今回は Claude Code の話

Codex CLI にゴリゴリOSSのソース読ませもしています(DeepWikiの代わりに)

あなた Python 分かってます?

  • Claudeは人類に比べて速く、安くPythonを書く

  • ただ私からはPythonを 理解しているとは言えない

  • Pythonを理解しているなら書かないコードを頻繁に書くため

例:f-string

  • フォーマット済み文字列リテラル

  • を含められる

>>> name = "りせ"
>>> f"こんにちは、{name}さん!"
'こんにちは、りせさん!'
>>> print(f"2 + 3 = {2 + 3}")
2 + 3 = 5

ロギングにf-stringは使わない [2]

f-stringの代わりに%-format
logger.info("%s - Something happened", user)
logger.error("Python version: %s", sys.version)

しかしClaudeは平気でf-stringを使う

f-stringでロギングしてはいけません [3]
logger.info(f"{user} - Something happened")
logger.error(f"Python version: {sys.version}")

💡リンタで指摘しよう

Ruff

  • Rustで書かれたPythonのリンタ兼フォーマッタ。速い [4]

  • flake8やpylint(リンタ)・black(フォーマッタ)からRuffへの置き換えが進む

$ uvx ruff check --fix --extend-select I && uvx ruff format

hatch fmt

  • Ruffのリントとフォーマットは現状別々のコマンド

  • Pythonプロジェクト管理ツール Hatch は、Ruffのリントとフォーマットを1コマンドで流せる [5]

  • 普通にRuffを流すよりも 厳しいルール

Claude Codeの フック を設定

  • Pythonを書いたときに hatch fmt を実行する

  • hatch fmt の出力を繰り返しClaudeに見せる(逃 し ま せ ん)

ローカルのプロジェクトに設定

.claude/
├── hooks/
│   └── python_format.sh
└── settings.local.json

PostToolUseフック

EditまたはWriteでPythonファイルを書いたら

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | { read file_path; if echo \"$file_path\" | grep -q '\\.py$'; then \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/python_format.sh \"$file_path\"; fi; }"
          }
        ]
      }
    ]
  }
}

python_format.sh

#!/usr/bin/env bash
set -u

hatch fmt "$1" 1>&2
exit_code=$?
if [ $exit_code -eq 1 ]; then
    exit 2
else
    exit $exit_code
fi

フックのエラーメッセージをClaudeに見せるために

  • python_format.sh の終了コードを 2 にする(Claude Codeをブロック)

  • hatch fmt の出力を stderr へ(Claudeが見る)

リファレンスの「Hook出力」参照 [6]

デモ

エラトステネスの篩をPythonで書いて

⎿ PostToolUse:Write hook returned blocking error: [jq -r
    '.tool_input.file_path' | { read file_path; if echo "$file_path"
    | grep -q '\.py$'; then
    "$CLAUDE_PROJECT_DIR"/.claude/hooks/python_format.sh
    "$file_path"; fi; }]: cmd [1] | ruff check --config
    '/.../hatch/env/.internal/
    hatch-static-analysis/.config/ukmfGEkg/ruff_defaults.toml' --fix
    /.../claude-code-harness
    /sieve_of_eratosthenes.py
    sieve_of_eratosthenes.py:11:12: PLR2004 Magic value used in
    comparison, consider replacing `2` with a constant variable
    sieve_of_eratosthenes.py:27:12: RET504 Unnecessary assignment to
    `primes` before `return` statement
    sieve_of_eratosthenes.py:35:5: T201 `print` found
    sieve_of_eratosthenes.py:36:5: T201 `print` found
    sieve_of_eratosthenes.py:37:5: T201 `print` found
    Found 5 errors.
    No fixes available (4 hidden fixes can be enabled with the
    `--unsafe-fixes` option).


⏺ Pythonのlintエラーが  08:24 PM  claude-sonnet-4-5-20250929
検出されました。修正
します。
当日生成された sieve_of_eratosthenes.py
MINIMUM_PRIME = 2


def sieve_of_eratosthenes(n):
    """
    エラトステネスの篩を使って、n以下の素数を全て求める

    Args:
        n: 上限値

    Returns:
        n以下の素数のリスト
    """
    if n < MINIMUM_PRIME:
        return []

    # 最初は全ての数を素数候補とする
    is_prime = [True] * (n + 1)
    is_prime[0] = is_prime[1] = False  # 0と1は素数ではない

    # 2からsqrt(n)まで処理
    for i in range(MINIMUM_PRIME, int(n**0.5) + 1):
        if is_prime[i]:
            # iの倍数を全て合成数としてマーク
            for j in range(i * i, n + 1, i):
                is_prime[j] = False

    # 素数のリストを作成して返す
    return [i for i in range(n + 1) if is_prime[i]]


# 使用例
def main():
    # 100以下の素数を求める
    n = 100
    primes = sieve_of_eratosthenes(n)
    print(f"{n}以下の素数:")  # noqa: T201
    print(primes)  # noqa: T201
    print(f"\n素数の個数: {len(primes)}")  # noqa: T201


if __name__ == "__main__":
    main()

時間があったら話すコンテンツ

  • Pythonでフックを書く

  • プラグインで配布

1️⃣ cchooks

inline script metadataと奇跡的相性🫶

#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "cchooks",
# ]
# ///
import subprocess

import cchooks

c = cchooks.create_context()

if c.tool_input.get("file_path", "").endswith(".py"):
    file_path = c.tool_input["file_path"]
    result = subprocess.run(["uvx", "hatch", "fmt", file_path], capture_output=True, text=True)
    if result.returncode == 1:  # Need to fix formatting
        cchooks.exit_block(f"Fix `hatch fmt` issues:\n{result.stdout}")
    else:
        cchooks.exit_success()

2️⃣ プラグイン

まとめ🌯 AIにPythonを理解したコードを書かせる試行錯誤

  • Claude Code、Pythonを理解してないよなあ

  • hatch fmt をPostToolUseフックに設定して逃さない

  • 引き続き磨き込んでいくぞ(コメント多い、型チェック)

ご清聴ありがとうございました

Happy AI-assisted development🤖

宣伝:10/30(木)31(金)AI駆動開発カンファレンス オンライン無料です!!