👻

Pythonの型ヒントと共に進化するコード(#3: 脆いスクリプト)

に公開
本連載の目次

出発点 〜 動くけど、触りたくないコード 〜

まず、以下のコードを読んでみてください。

このコードは、郵便番号を受け取り、外部 API(架空)から住所情報を取得して整形するスクリプトです。
zipcode="1000001" のような正常系では期待どおりに動作します。

ただし、型ヒントは一切定義されていない状態です。

address_fetcher.py (最初のコード)
import requests
import json

def _build_full_address(address_data):
    """
    住所データからフル住所文字列を生成する。
    """
    return address_data["prefecture"] + address_data["city"] + address_data["town"]

def fetch_and_format_address(zipcode, include_kana):
    """郵便番号から住所を取得し、整形して返す"""

    # APIエンドポイントのURLを定義(架空の API)
    api_url = "https://api.zipcode-jp.example/v1/address"

    try:
        # 郵便番号検索APIにリクエスト
        response = requests.post(api_url, json={"zipcode": zipcode})
        if response.status_code != 200:
            print(f"Error: Failed to fetch address. Status: {response.status_code}")
            return None
        address_data = response.json()  # 結果を JSON 形式にする

        # フル住所を生成
        full_address = _build_full_address(address_data)

        # 結果を組み立て
        result = {
            "zipcode": zipcode,
            "full_address": full_address,
            "prefecture": address_data["prefecture"],
            "city": address_data["city"],
            "town": address_data["town"],
        }

        # カナを含める場合は、取得結果に対して各カナ情報を連結し、 full_address_kana にする
        if include_kana:
            result["full_address_kana"] = (
                address_data["prefecture_kana"]
                + address_data["city_kana"]
                + address_data["town_kana"]
            )

        # 結果を JSON 形式で返す
        return json.dumps(result, indent=2, ensure_ascii=False)

    except requests.exceptions.RequestException as e:
        # APIリクエスト時の例外を処理
        print(f"An error occurred during API request: {e}")
        return None
    except (KeyError, IndexError) as e:
        # データアクセス時の例外を処理
        print(f"Error processing data: Invalid data structure - {e}")
        return None

# 実行例
# ここでは便宜上、本スクリプト内で上記関数を呼び出していますが、
# 本来は別のファイルに呼び出し処理があることを想定してください。
if __name__ == "__main__":
    result = fetch_and_format_address("1000001", include_kana=True)
    if result is not None:
        print(result)

どうでしょうか。やっていることはとても単純です。
一見すると問題なく動きそうに見えるかもしれません。

しかし、もしあなたがこのスクリプトの保守担当者になったとしたら将来どんな問題が起きそうだと感じますか?
変更や機能追加を依頼されたとき、どこへ手を入れるのが怖いでしょうか。

ここで、2 日目の記事で紹介したstrict = trueの設定で型チェッカー mypy を実行してみましょう。

mypy address_fetcher.py

結果は次のとおりです。

mypy address_fetcher.py

# 結果
address_fetcher.py:4: error: Function is missing a type annotation  [no-untyped-def]
address_fetcher.py:10: error: Function is missing a type annotation  [no-untyped-def]
address_fetcher.py:25: error: Call to untyped function "_build_full_address" in typed context  [no-untyped-call]
address_fetcher.py:58: error: Call to untyped function "fetch_and_format_address" in typed context  [no-untyped-call]
Found 4 errors in 1 file (checked 1 source file)

型ヒントがないため、mypy は関数の引数や戻り値の型を推論できずエラーを報告しています。

どんなエラーか見てみましょう。

エラーの内容を読み解く

まずは以下です。

address_fetcher.py:4: error: Function is missing a type annotation  [no-untyped-def]
address_fetcher.py:10: error: Function is missing a type annotation  [no-untyped-def]

[no-untyped-def]関数に型注釈がないというエラーです。
_build_full_addressfetch_and_format_address の両関数に、引数と戻り値の型ヒントがないことを指摘しています。

address_fetcher.py:25: error: Call to untyped function "_build_full_address" in typed context  [no-untyped-call]
address_fetcher.py:58: error: Call to untyped function "fetch_and_format_address" in typed context  [no-untyped-call]

[no-untyped-call]型注釈のない関数を呼び出しているというエラーです。strict モードでは、型のない関数を呼び出すこと自体が警告対象になります。

つまり mypy は このコードの型安全性は何も保証できません と言っているようなものです。

なぜこのスクリプトが「脆い」のか

型の保証がないということは、例えば以下のような問題が潜んでいても気づけないということです。

  • 関数に想定外の型の値を渡してしまう
  • 戻り値の型を誤解したまま使い続ける
  • 辞書のキー名をタイポしても実行するまで気付けない

コードは動いているように見えても上記で示した問題はいつ表面化するかわかりません。

そしてこのような問題はコードベースが大きくなるほど発見が難しくなり、修正コストも膨らんでいくことは想像に容易いと思います。

もちろん、テストが完璧で一切保守しないのであれば話は別かもしれません。
しかし現実にはコードに一切変更が加えられない保証はどこにもありません。

これが「動くけれど脆い」と呼ぶ理由です。

ただ、安心してください

Python には上で挙げた問題を解決するための型ヒントがありますよ!!

この「動くけれど脆い」スクリプトに対して型ヒントを少しずつ導入しながら堅牢で保守しやすいコードに育てていきます。

次回予告

大変お待たせしました!
次の章からこのコードの問題を解決する第一歩として TypedDict を導入していきます。

Discussion