閉じる

Pythonのassert文でバグを早期発見する基本と使い方

開発中に「ここは絶対にこうなっているはず」という仮定が崩れた瞬間を、その場で知らせてくれるのがassert文です。

複雑なロジックでも、意図と前提をコード化することで、バグを早期にあぶり出せます。

本記事ではPythonのassert文の基本から使いどころ、注意点、他手法との使い分けまでを初心者向けに丁寧に解説します。

Pythonのassert文とは

assertの役割と動作(AssertionError)

assert文は「開発者が想定した前提(仮定)が満たされているか」を実行時に確認するための仕組みです。

条件が偽であればAssertionErrorを送出してプログラムを止めます。

これにより、原因が広がる前にバグを表面化させられます。

Python
# 0で割らないという前提をassertで明示する例
def inverse(n: float) -> float:
    assert n != 0, "nは0以外である必要があります"
    return 1 / n

print(inverse(2))
print(inverse(0))  # ここでAssertionError
実行結果
0.5
Traceback (most recent call last):
  ...
AssertionError: nは0以外である必要があります

assertは「想定が破れたら即座に落とす」ための道具であり、ユーザー入力などの通常のエラー処理とは目的が異なります。

基本構文とメッセージの書き方

assertの基本形は2通りです。

メッセージは省略可能ですが、原因がわかる短文を付けると、後から読み返したときに役立ちます。

  • assert 条件式
  • assert 条件式, "失敗したときに表示する説明文"
Python
# 例: 条件が成り立つ場合は何も起きずに先へ進みます
x = 5
assert x > 0, "xは正の数である必要があります"
print("OK")  # 到達します

# 例: 条件が偽だとAssertionError
y = -1
assert y > 0, f"yは正の数である必要がありますが、{y}でした"
print("この行は実行されません")
実行結果
OK
Traceback (most recent call last):
  ...
AssertionError: yは正の数である必要がありますが、-1でした

いつ使うか(開発中の仮定チェック)

assertは「開発中に埋め込む安全ネット」として使います。

典型例は次の通りです。

関数の引数の前提、戻り値の後条件、ループ中の不変条件、到達しないはずの分岐、簡易テストの自己検査などです。

これらは、「ここは絶対にこうであるべき」という開発者の仮定をコードとして表現したものです。

assertの使いどころと具体例

関数の前提条件をチェック(引数の範囲など)

関数の入口で引数の前提を明示すると、意図が伝わりやすく、破れた瞬間に素早く発見できます。

Python
# 価格と割引率の前提をassertで明示
def apply_discount(price: float, rate: float) -> float:
    # 型や値域の前提
    assert isinstance(price, (int, float)), "priceは数値である必要があります"
    assert price >= 0, f"priceは0以上のはずですが、{price}でした"
    assert 0.0 <= rate <= 1.0, f"rateは0.0〜1.0のはずですが、{rate}でした"
    return float(price) * (1.0 - float(rate))

print(apply_discount(1000, 0.2))  # OK
print(apply_discount(-100, 0.2))  # 前提違反
実行結果
800.0
Traceback (most recent call last):
  ...
AssertionError: priceは0以上のはずですが、-100でした

戻り値の後条件を検証

実装の途中でバグが紛れ込んでも、戻り値の性質をassertで守っておけば早く気付けます。

Python
# 戻り値が0.0〜1.0に収まることを後条件として保証
def clamp01(x: float) -> float:
    if x < 0.0:
        x = 0.0
    if x > 1.0:
        x = 1.0
    assert 0.0 <= x <= 1.0, "clamp01の戻り値は0.0〜1.0の範囲のはず"
    return x

print(clamp01(1.2))
print(clamp01(-0.5))
実行結果
1.0
0.0

もう一つの例: 付帯する関係を保証

Python
# ソート済みかつ重複なしという性質を後条件で確認
def sorted_unique(nums: list[int]) -> list[int]:
    result = sorted(set(nums))
    # ソート済みであること(恒真に見えるが、実装変更時の崩れ検知に効く)
    assert result == sorted(result), "戻り値は昇順である必要があります"
    return result

print(sorted_unique([3, 1, 2, 2]))
実行結果
[1, 2, 3]

ループや状態の不変条件を確認

処理の途中で常に成り立つはずの条件(不変条件)を埋め込むと、意図とズレた瞬間に止められます。

Python
# 非負の数列を加算していくと合計は常に非負のはず、という不変条件
def sum_non_negative(seq: list[int]) -> int:
    total = 0
    for i, n in enumerate(seq, start=1):
        total += n
        assert total >= 0, f"合計は非負のはずが負になりました(index={i}, total={total})"
    return total

print(sum_non_negative([3, 2, 1]))      # OK
print(sum_non_negative([3, -5, 4]))     # 不変条件違反
実行結果
6
Traceback (most recent call last):
  ...
AssertionError: 合計は非負のはずが負になりました(index=2, total=-2)

到達しないはずの分岐を検知

列挙型や限られた文字列の取り得る値が想定外になったら即失敗させます。

Python
def handle_status(status: str) -> str:
    if status == "ok":
        return "処理完了"
    elif status == "retry":
        return "再試行します"
    elif status == "fail":
        return "失敗しました"
    # ここに来るのは想定外
    assert False, f"未知のstatusを検知しました: {status}"

print(handle_status("ok"))
print(handle_status("unknown"))  # 想定外
実行結果
処理完了
Traceback (most recent call last):
  ...
AssertionError: 未知のstatusを検知しました: unknown

簡易テストで処理の期待値を確認

小さな自己検査をファイル末尾に置いて、処理の期待値が崩れていないかを常時チェックできます。

単体テストの代替ではありませんが、実装しながらの即時確認に有効です。

Python
def factorial(n: int) -> int:
    assert n >= 0, "nは0以上"
    result = 1
    for i in range(2, n + 1):
        result *= i
    # 後条件: 常に正の整数
    assert result > 0, "factorialの戻り値は正の整数のはず"
    return result

# 簡易自己検査
assert factorial(0) == 1
assert factorial(5) == 120
実行結果
# 上記はすべてassertが通れば出力はありません(静かに成功)

assertの注意点とベストプラクティス

本番では無効化される(-O)ことを理解

重要

assertは最適化オプションで無効化されます

python -Oで実行するとassert文が丸ごと取り除かれ、__debug__がFalseになります。

assertにビジネスロジック上の必須処理を入れてはいけません

Python
print("__debug__ =", __debug__)
assert False, "このassertはデフォルトでは発火します"
print("ここまで到達したらassertは無効化されています")
実行結果
# 通常実行
__debug__ = True
Traceback (most recent call last):
  ...
AssertionError: このassertはデフォルトでは発火します
実行結果
# -Oで実行
__debug__ = False
ここまで到達したらassertは無効化されています

入力バリデーションには使わない(ifやraiseで対応)

ユーザー入力や外部データの検証は常に実行されるべきなので、assertではなくifraiseで行います。

Python
def read_age(text: str) -> int:
    if not text.isdecimal():
        raise ValueError(f"年齢は整数である必要があります: {text}")
    age = int(text)
    if not (0 <= age <= 130):
        raise ValueError(f"年齢の範囲が不正です: {age}")
    return age

print(read_age("25"))
print(read_age("二十"))  # ValueErrorで適切に通知
実行結果
25
Traceback (most recent call last):
  ...
ValueError: 年齢は整数である必要があります: 二十

try-exceptとassertの違い

  • assertは「想定外を露呈させるために落とす」ものです。回復処理を意図しません。
  • try-exceptは「想定内の例外を捕まえて回復や代替処理をする」ための仕組みです。
Python
# ファイル読み込みは失敗が想定内 → try-exceptで処理
try:
    with open("config.json", "r", encoding="utf-8") as f:
        config = f.read()
except FileNotFoundError:
    config = "{}"  # 既定値で継続

# 一方で、到達不能分岐は想定外 → assertで停止
state = "unknown"
assert state in {"init", "running", "stopped"}, f"未知の状態: {state}"

副作用のある式を入れない

assert式の中で副作用を起こす処理を絶対に書かないでください

-Oで実行すると式自体が評価されず、副作用が起きなくなります

Python
# 悪い例: -Oだとinsertが呼ばれず、データが保存されません
def add_user_bad(db, user):
    assert db.insert(user) == 1, "1件だけ挿入されるはず"

# 良い例: 必須処理はassertの外で実行し、結果だけ検査する
def add_user_good(db, user):
    affected = db.insert(user)  # これは常に実行される
    assert affected == 1, f"挿入件数が想定外: {affected}"

メッセージは原因がわかる短文で書く

メッセージには「何が、どうあるべきか、実際はいくつか」を短く含めると役立ちます。

  • 悪い例: "エラー"
  • 良い例: f"priceは0以上のはずですが、{price}でした"
Python
def withdraw(balance: int, amount: int) -> int:
    assert amount > 0, f"amountは正の整数のはずですが、{amount}でした"
    assert balance >= amount, f"残高不足 balance={balance}, amount={amount}"
    return balance - amount

パフォーマンスへの配慮(高コストなチェックは避ける)

assertは開発時の安全網なので、正確さが最優先ですが、ホットパスで重い検査を毎回行うと遅くなります。

必要に応じてチェック頻度を落としたり、__debug__で囲んでデバッグ時のみ重い検査を有効にする方法があります。

Python
def process(data: list[int]) -> int:
    # 本処理
    total = sum(data)

    # デバッグ時のみ重い整合性検査を実行
    if __debug__:
        # 例えばO(n)の重い検査
        assert all(isinstance(x, int) for x in data), "dataにはintのみのはず"

    return total

assertと他のデバッグ手法の使い分け

if + raiseとの比較(常時有効な検証)

ユーザー入力や外部要因による失敗はif+raiseで扱います。

assertは無効化されるため、運用上必要な検証には不向きです。

逆に、内部不整合や「起きないはず」の条件はassertが適切です。

Python
# 常時必要な検証
def load_port(env: dict) -> int:
    port_s = env.get("PORT")
    if port_s is None:
        raise RuntimeError("PORTが設定されていません")
    port = int(port_s)
    if not (0 < port < 65536):
        raise ValueError(f"PORTの範囲が不正: {port}")
    return port

# 内部前提の検査
def start_server(port: int) -> None:
    assert 0 < port < 65536, f"無効なport: {port}"
    print(f"Listening on {port}")

start_server(load_port({"PORT": "8080"}))
実行結果
Listening on 8080

loggingとの併用で原因を記録

assertは即座に落としますが、状況証拠はログに残しておくと調査が楽です。

assertの直前で重要な値をloggingに出すのがおすすめです。

Python
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")

def compute_ratio(part: int, whole: int) -> float:
    if whole == 0:
        logging.error("wholeが0。入力: part=%s, whole=%s", part, whole)
        raise ZeroDivisionError("wholeは0以外である必要があります")
    ratio = part / whole
    logging.debug("ratio=%s", ratio)
    # 内部前提(比は0.0〜1.0に収まる設計)を確認
    assert 0.0 <= ratio <= 1.0, f"比が範囲外: ratio={ratio}, part={part}, whole={whole}"
    return ratio

print(compute_ratio(2, 5))
実行結果
0.4

単体テストとassertの関係(補助的に使う)

単体テスト(unittestやpytest)は仕様に対する網羅的な検証を行う道具です。

一方assertは実装内部の仮定を早期に検知する補助線です。

実装コードの中のassertは、テストがカバーしきれない内部不整合の早期検出に役立ち、テストコード側ではpytestがassertを活用して失敗時に差分を見やすく表示してくれます。

下表は使い分けの要点です。

手法実行環境での有効性主な目的代表コード本番での推奨度
assert-Oで無効化される内部前提の破れ検知assert cond, "why"サポート的に使用
if + raise常時有効入力バリデーション・回復不能エラー通知if not cond: raise ValueError()強く推奨
logging常時有効状況の記録・原因追跡logging.info(...)強く推奨
単体テスト開発/CIで常時仕様の自動検証assert func(x)==yなど強く推奨

まとめ

assertは「コード内の仮定」を実行時に検査し、崩れた瞬間に露見させる強力な安全網です。

前提条件・後条件・不変条件・到達不能分岐・簡易自己検査に活用すれば、原因が広がる前に不具合を捉えられます。

一方で本番では無効化される点を忘れず、ユーザー入力の検証や回復処理はifraiseで行いましょう。

副作用をassert内に書かない、メッセージは短く具体的に、重い検査は必要に応じて__debug__で限定する、といったベストプラクティスを守ることで、「落ちるなら早く・分かりやすく」を実現できます。

assertを適切に使い分け、loggingや単体テストと組み合わせることで、開発効率と品質を着実に高めていきましょう。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!