8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

「Pythonは動的型付けだから型なんて書かなくていい」

そう思ってた時期が私にもありました。

でも型ヒントを真面目に書くようになってから、実行前にバグが見つかるようになって、生産性が爆上がりした。

型ヒントなしだとこうなる

def calculate_total(items, tax_rate):
    """商品リストから合計金額を計算"""
    total = 0
    for item in items:
        total += item["price"] * item["quantity"]
    return total * (1 + tax_rate)

一見普通の関数。でも使う側がミスると...

# バグ1: tax_rateに文字列を渡す
calculate_total(products, "10%")
# => TypeError: unsupported operand type(s) for +: 'int' and 'str'

# バグ2: itemsに文字列を渡す
calculate_total("りんご3個", 0.1)
# => TypeError: string indices must be integers, not 'str'

# バグ3: Noneを渡す
calculate_total(None, 0.1)
# => TypeError: 'NoneType' object is not iterable

全部実行時エラー。本番環境で初めて気づくパターン。


型ヒントありだとこうなる

from typing import TypedDict

class Product(TypedDict):
    name: str
    price: int
    quantity: int

def calculate_total(items: list[Product], tax_rate: float) -> float:
    total = 0
    for item in items:
        total += item["price"] * item["quantity"]
    return total * (1 + tax_rate)

mypyで静的解析:

$ mypy mypy_test.py
mypy_test.py:22: error: Argument 2 has incompatible type "str"; expected "float"
mypy_test.py:23: error: Argument 1 has incompatible type "str"; expected "list[Product]"
mypy_test.py:24: error: Argument 1 has incompatible type "None"; expected "list[Product]"
Found 3 errors in 1 file

実行する前にバグが見つかる!


型ヒント完全ガイド

基本の型

def greet(name: str) -> str:
    return f"Hello, {name}!"

def add(a: int, b: int) -> int:
    return a + b

def is_valid(flag: bool) -> bool:
    return flag

Optional(None許容)

from typing import Optional

def find_user(user_id: int) -> Optional[str]:
    """ユーザーを検索。見つからなければNone"""
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

# 使う側
user = find_user(1)
if user is not None:  # 型ガード
    print(user.upper())  # ここではstrとして扱える

Union(複数の型)

from typing import Union

def format_id(id_value: Union[int, str]) -> str:
    if isinstance(id_value, int):
        return f"ID-{id_value:05d}"
    return f"ID-{id_value}"

# Python 3.10+では | 記法が使える
def format_id_new(id_value: int | str) -> str:
    return f"ID-{id_value}"

リスト・辞書

def process_scores(scores: list[int]) -> dict[str, float]:
    return {
        "average": sum(scores) / len(scores),
        "max": float(max(scores)),
        "min": float(min(scores)),
    }

コールバック関数

from typing import Callable

def apply_operation(
    values: list[int],
    operation: Callable[[int], int]  # int -> int の関数
) -> list[int]:
    return [operation(v) for v in values]

doubled = apply_operation([1, 2, 3], lambda x: x * 2)
# => [2, 4, 6]

ジェネリクス

from typing import TypeVar, Optional

T = TypeVar('T')

def first_element(items: list[T]) -> Optional[T]:
    """リストの最初の要素を返す"""
    return items[0] if items else None

# 型が自動推論される
num = first_element([1, 2, 3])      # int | None
text = first_element(["a", "b"])   # str | None

TypedDict(辞書の構造定義)

from typing import TypedDict, NotRequired

class APIResponse(TypedDict):
    status: str
    data: dict
    error: NotRequired[str]  # オプショナルなキー

def handle_response(response: APIResponse) -> None:
    print(f"Status: {response['status']}")
    if 'error' in response:
        print(f"Error: {response['error']}")

Literal(リテラル型)

from typing import Literal

def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None:
    print(f"Log level: {level}")

set_log_level("INFO")      # OK
set_log_level("VERBOSE")   # mypyエラー!

Final(定数)

from typing import Final

MAX_CONNECTIONS: Final = 100
MAX_CONNECTIONS = 200  # mypyエラー!再代入禁止

dataclassと組み合わせると最強

from dataclasses import dataclass
from typing import Optional
from datetime import datetime

@dataclass
class User:
    id: int
    name: str
    email: Optional[str] = None
    created_at: datetime = datetime.now()
    
    def is_guest(self) -> bool:
        return self.email is None

user = User(id=1, name="Alice", email="alice@example.com")
print(user.is_guest())  # => False
  • 型が明確
  • __init__, __repr__, __eq__ が自動生成
  • IDEの補完が効く

mypyの使い方

インストール

pip install mypy

実行

# 単一ファイル
mypy your_script.py

# ディレクトリ全体
mypy src/

# 厳格モード
mypy --strict your_script.py

pyproject.tomlで設定

[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true

VSCodeとの連携

settings.jsonに追加:

{
  "python.analysis.typeCheckingMode": "basic",
  "mypy-type-checker.args": ["--strict"]
}

よくある疑問

Q: 実行時に型チェックされる?

されません。型ヒントは静的解析ツール(mypy, Pyright)のためのもの。

実行時チェックが必要なら pydanticbeartype を使います。

Q: 外部ライブラリの型が無い

types-xxx パッケージをインストール:

pip install types-requests types-redis

Q: 型が複雑になりすぎる

型エイリアスを使う:

from typing import TypeAlias

UserId: TypeAlias = int
UserDict: TypeAlias = dict[UserId, str]

def get_user(users: UserDict, user_id: UserId) -> str | None:
    return users.get(user_id)

まとめ

メリット 説明
🐛 バグ早期発見 実行前に型エラーを検出
📝 ドキュメント 関数の使い方が明確に
🔧 リファクタリング 型の変更が影響する箇所がわかる
💡 IDE補完 入力候補が正確に
# 型ヒントを書く習慣をつけよう
def your_function(arg: int) -> str:
    ...
8
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?