Pythonのassertとは、「ここではこの条件が必ず成り立つはず」という前提をコードに書き込み、成り立たなければ AssertionError で止める文です。 assert 条件, メッセージ の形で書き、条件が偽のときだけ例外が発生します。

assert は便利な一方で、python -O を付けて実行すると、すべての assert が実行されなくなるという性質があります。このため「どこに使ってよく、どこには向かないか」を知っておくことが大切です。この記事では、AssertionError のトレースバックを実際の出力で示し、-O と環境変数 PYTHONOPTIMIZE で assert が消える様子を実行結果で比べます。assert(条件, メッセージ) と括弧を付けると常に真になる罠、副作用のある式を書いたときの不具合も再現しました。後半には assert・if と raise・pytest・unittest の使い分け表と、場面ごとの一覧を載せています。コードはすべて Python 3.14.7 で実行して確かめたものです。

assertの早見表

知りたいこと 答え
基本の形 assert 条件
メッセージ付き assert 条件, "メッセージ"(カンマで区切る。括弧では囲まない)
条件が真のとき 何も起きず、次の行へ進む
条件が偽のとき AssertionError が発生する。メッセージがあれば AssertionError: メッセージ
同じ意味のコード if __debug__: の中で if not 条件: raise AssertionError(メッセージ)
無効になる条件 python -O、python -OO、環境変数 PYTHONOPTIMIZE を設定して実行したとき
無効かどうかを調べる __debug__(通常は True、-O では False)、sys.flags.optimize
例外のクラス AssertionError(Exception の子クラスなので except で受け取れる)
括弧を付けた assert(条件, "メッセージ") 要素2つのタプルと解釈され、常に真になる。SyntaxWarning が出る

どれを使うかの早見表は次のとおりです。

場面 使うもの
自分のコードの前提(ここには来ないはず、この値は正のはず)を確かめる assert
ユーザーの入力、ファイルやAPIから受け取った値を確かめる if と raise ValueError(...)
権限やログイン状態を確かめる if と raise PermissionError(...)
テストコードで結果を確かめる(pytest) assert 結果 == 期待値
テストコードで結果を確かめる(unittest) self.assertEqual(結果, 期待値)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python assertの使い方|AssertionErrorと使いどころ」

assertの基本の書き方とAssertionError

いちばん短い形は assert 条件 です。条件が偽になると、AssertionError が発生してプログラムが止まります。

stock = 3
assert stock >= 5
print("この行は実行されない")
$ python basic.py
Traceback (most recent call last):
  File "basic.py", line 2, in <module>
    assert stock >= 5
           ^^^^^^^^^^
AssertionError

(トレースバックのファイルのパスは、ファイル名だけに短くして載せています。以降も同じです。)

メッセージがないと AssertionError とだけ表示されます。ただし Python 3.11 以降は、偽になった式の下に ^ の印が付くので、どの条件で止まったかは読み取れます。

カンマの後ろにメッセージを書くと、原因がさらに伝わりやすくなります。

def average(scores):
    assert len(scores) > 0, "scores が空です"
    return sum(scores) / len(scores)


print(average([80, 90]))
print(average([]))
$ python message.py
85.0
Traceback (most recent call last):
  File "message.py", line 7, in <module>
    print(average([]))
          ~~~~~~~^^^^
  File "message.py", line 2, in average
    assert len(scores) > 0, "scores が空です"
           ^^^^^^^^^^^^^^^
AssertionError: scores が空です

1回目の呼び出しは条件が真なので何も起きず、85.0 が表示されました。2回目は空のリストを渡したため、AssertionError: scores が空です で止まっています。トレースバックは「どの呼び出しから(7行目)」「どの assert で(2行目)」止まったかを上から順に示しています。

assertと同じ意味のコード

今回開いた公式の言語リファレンス(単純文の assert 文の節)では、assert 式1, 式2 は次のコードと等価だと説明されています。

if __debug__:
    if not 式1:
        raise AssertionError(式2)

ここから、次の3つのことが読み取れます。

  • 条件が偽のときだけ例外が発生します。 not 式1 が真、つまり式1が偽と判定されたときです。
  • メッセージ(式2)は AssertionError の引数になります。 文字列に限らず、どんな値でも渡せます。
  • 全体が if __debug__: の中にあります。 __debug__ が False になると、assert は丸ごと実行されなくなります。これが -O で無効になる仕組みです。

偽と判定される値

assert は条件を真偽値として判定します。比較の式だけでなく、値そのものを書くこともできますが、0 や空のリストも偽と判定される点に気をつけます。

for value in (0, 0.0, "", [], None, "0", [0], -1):
    try:
        assert value
        result = "通過"
    except AssertionError:
        result = "AssertionError"
    print(f"assert {value!r:<5} -> {result}")
assert 0     -> AssertionError
assert 0.0   -> AssertionError
assert ''    -> AssertionError
assert []    -> AssertionError
assert None  -> AssertionError
assert '0'   -> 通過
assert [0]   -> 通過
assert -1    -> 通過

「None でないこと」を確かめたいときに assert value と書くと、0 や空文字のときにも止まります。assert value is not None と、確かめたい条件をそのまま書くと意図どおりになります。

メッセージの書き方

メッセージには f-string を使って、そのときの値を入れておくと、止まった理由がひと目でわかります。

def check(x):
    assert x == 4, f"x は 4 のはずですが {x!r} でした"


def describe():
    print("  (メッセージの式が評価された)")
    return "条件が偽でした"


try:
    check(3)
except AssertionError as e:
    print("1:", e)

try:
    assert 3 == 4, {"actual": 3, "expected": 4}      # 文字列以外も渡せる
except AssertionError as e:
    print("2:", e.args)

assert 1 == 1, describe()
print("3: 条件が真なら、メッセージの式は評価されない")

try:
    assert 1 == 2, describe()
except AssertionError as e:
    print("4:", e)

print("5:", issubclass(AssertionError, Exception))
1: x は 4 のはずですが 3 でした
2: ({'actual': 3, 'expected': 4},)
3: 条件が真なら、メッセージの式は評価されない
  (メッセージの式が評価された)
4: 条件が偽でした
5: True

3 の前には「評価された」の行が出ておらず、4 の前にだけ出ています。メッセージの式は条件が偽のときにだけ評価されます。 値を整形する処理をメッセージに書いても、条件が真のあいだは実行されません。

メッセージが長くて1行に収まらないときは、メッセージの側だけを括弧で囲みます。

age = -5
assert age >= 0, (
    "年齢は0以上にしてください: "
    f"age={age}"
)
$ python long_message.py
Traceback (most recent call last):
  File "long_message.py", line 2, in <module>
    assert age >= 0, (
           ^^^^^^^^
AssertionError: 年齢は0以上にしてください: age=-5

python -OとPYTHONOPTIMIZEでassertが消える

assert は、Python を最適化モードで起動すると実行されなくなります。次のファイルを、起動方法を変えて実行します。

import sys

print("__debug__ =", __debug__, "/ sys.flags.optimize =", sys.flags.optimize)

if __debug__:
    print("デバッグ用の処理(-O では消える)")

assert 1 + 1 == 3, "計算が合いません"
print("最後まで実行された")

通常の実行では、assert で止まります。

$ python debug_flag.py
__debug__ = True / sys.flags.optimize = 0
デバッグ用の処理(-O では消える)
Traceback (most recent call last):
  File "debug_flag.py", line 8, in <module>
    assert 1 + 1 == 3, "計算が合いません"
           ^^^^^^^^^^
AssertionError: 計算が合いません

-O を付けると、assert は実行されず、最後の行まで進みます。

$ python -O debug_flag.py
__debug__ = False / sys.flags.optimize = 1
最後まで実行された

環境変数 PYTHONOPTIMIZE を設定しても、同じ結果になります。コマンドを書き換えなくても有効になる点が -O との違いです。

$ PYTHONOPTIMIZE=1 python debug_flag.py
__debug__ = False / sys.flags.optimize = 1
最後まで実行された

起動方法ごとの結果をまとめます。すべて実行して確かめた値です。

起動方法 __debug__ sys.flags.optimize assert docstring
python file.py True 0 実行される 残る
python -O file.py False 1 実行されない 残る
python -OO file.py False 2 実行されない 取り除かれる
PYTHONOPTIMIZE=1 python file.py False 1 実行されない 残る
PYTHONOPTIMIZE=2 python file.py False 2 実行されない 取り除かれる
PYTHONOPTIMIZE=yes python file.py False 1 実行されない 残る
PYTHONOPTIMIZE=0 python file.py True 0 実行される 残る

docstring の列も、関数の __doc__ を表示して確かめた結果です(取り除かれると None になります)。PYTHONOPTIMIZE については、今回開いた公式ドキュメントに「空でない文字列を設定すると -O と同じ、整数を設定すると -O をその回数だけ指定したのと同じ」と書かれており、上の表の結果と一致しました。

上の例は Linux のシェルでの書き方です。Windows のコマンドプロンプトでは set PYTHONOPTIMIZE=1、PowerShell では $env:PYTHONOPTIMIZE = "1" を実行してから python を起動します(Windows では未実行です)。コンテナや実行環境の設定に PYTHONOPTIMIZE が入っていると、コードを変えていなくても assert が無効になります。 「手元では止まるのに、別の環境では止まらない」ときは、__debug__ の値を表示して確かめます。

バイトコードから消えていることを確かめる

「実行されない」とは、条件の判定を飛ばすのではなく、assert の命令そのものが作られないという意味です。dis モジュールで、コンパイル結果を見比べます。

import dis

source = "assert x > 0, 'x は正の数'"
for level in (0, 1):
    print(f"--- optimize={level}")
    dis.dis(compile(source, "<demo>", "exec", optimize=level))
--- optimize=0
  0           RESUME                   0

  1           LOAD_NAME                0 (x)
              LOAD_SMALL_INT           0
              COMPARE_OP             148 (bool(>))
              POP_JUMP_IF_TRUE         8 (to L1)
              NOT_TAKEN
              LOAD_COMMON_CONSTANT     0 (AssertionError)
              LOAD_CONST               1 ('x は正の数')
              CALL                     0
              RAISE_VARARGS            1
      L1:     LOAD_CONST               2 (None)
              RETURN_VALUE
--- optimize=1
          RESUME                   0
          LOAD_CONST               0 (None)
          RETURN_VALUE

optimize=0 では、比較(COMPARE_OP)、AssertionError の読み込み、RAISE_VARARGS が並んでいます。optimize=1 では None を返す命令だけになり、条件の x > 0 を評価する命令もなくなっています。 命令の名前や並びは Python のバージョンによって変わります。

-O で読み込んだモジュールは、キャッシュのファイル名も変わります。mod.py を3通りの方法で読み込んだあとの __pycache__ には、mod.cpython-314.pyc、mod.cpython-314.opt-1.pyc、mod.cpython-314.opt-2.pyc の3つができていました。

__debug__ には代入できない

__debug__ は Python の起動時に決まり、プログラムの中からは変えられません。

$ python -c "__debug__ = False"
  File "<string>", line 1
SyntaxError: cannot assign to __debug__

実行中に「assert を有効にする・無効にする」を切り替える方法はなく、決めるのは起動方法だけです。

罠1:assert(条件, メッセージ)は常に真になる

assert は関数ではなく文です。print() と同じ感覚で括弧を付けると、意味が変わります。

age = -5
assert(age >= 0, "年齢は0以上にしてください")
print("通過しました: age =", age)
$ python tuple_trap.py
tuple_trap.py:2: SyntaxWarning: assertion is always true, perhaps remove parentheses?
  assert(age >= 0, "年齢は0以上にしてください")
通過しました: age = -5

age は -5 なのに、assert を通過しています。(age >= 0, "年齢は0以上にしてください") は要素が2つのタプルと解釈され、空でないタプルは常に真と判定されるためです。条件の age >= 0 が False であっても、(False, "…") というタプルは真です。

Python はこの書き方を見つけると、SyntaxWarning: assertion is always true, perhaps remove parentheses?(assert が常に真になります。括弧を取り除きませんか)と知らせてくれます。同じ警告は Python 3.11.15 でも表示されました。警告は表示されるだけで実行は続くので、ログに流れて見落としやすい点に気をつけます。

書き方 解釈 結果
assert age >= 0, "メッセージ" 条件とメッセージ 意図どおり
assert (age >= 0), "メッセージ" 条件だけを括弧で囲んでいる 意図どおり
assert age >= 0, ("長い" "メッセージ") メッセージだけを括弧で囲んでいる 意図どおり
assert(age >= 0, "メッセージ") 要素2つのタプル 常に真。SyntaxWarning
assert (age >= 0, "メッセージ") 同上(空白があっても同じ) 常に真。SyntaxWarning

警告をエラーとして扱うと、見落としを防げます。-W error を付けて実行した結果です。

$ python -W error tuple_trap.py
  File "tuple_trap.py", line 2
    assert(age >= 0, "年齢は0以上にしてください")
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: assertion is always true, perhaps remove parentheses?

テストやCIで -W error を付けておくと、この書き方が入った時点で SyntaxError として止まります。

罠2:副作用のある式を書くと動きが変わる

assert の条件に、値を変える処理(副作用)を書くと、-O のときだけ結果が変わります。

tasks = ["掃除", "洗濯", "買い物"]

# リストから取り出す処理を assert の中に書いた例
assert tasks.pop() == "買い物", "最後は買い物のはず"

print("残り:", tasks)
$ python side_effect.py
残り: ['掃除', '洗濯']
$ python -O side_effect.py
残り: ['掃除', '洗濯', '買い物']

通常の実行では pop() で「買い物」が取り出されますが、-O では assert ごと消えるため、pop() も実行されず、リストに3つ残ったままになります。エラーは出ず、結果だけが変わるので、原因を見つけるのに時間がかかる不具合です。

直し方は、処理と確認を分けることです。

last = tasks.pop()                              # 処理は assert の外に書く
assert last == "買い物", "最後は買い物のはず"   # assert は確認だけ

同じ理由で、assert save(data)、assert cursor.execute(sql)、assert queue.get() のように、呼び出すこと自体に意味がある関数を assert の中に書かないようにします。assert の条件は「消えても動きが変わらない式」だけにする、と覚えておくと安心です。

罠3:入力検証や権限チェックに使うと素通りする

管理者だけが実行できる処理を、assert で守った場合と if と raise で守った場合を比べます。

def delete_user(current_user, target):
    assert current_user["is_admin"], "管理者だけが実行できます"
    print(f"{target} を削除しました(実行者: {current_user['name']})")


def delete_user_safe(current_user, target):
    if not current_user["is_admin"]:
        raise PermissionError("管理者だけが実行できます")
    print(f"{target} を削除しました(実行者: {current_user['name']})")


guest = {"name": "guest", "is_admin": False}
for func in (delete_user, delete_user_safe):
    try:
        func(guest, "user-001")
    except (AssertionError, PermissionError) as e:
        print(f"{func.__name__}: {type(e).__name__}: {e}")
$ python permission.py
delete_user: AssertionError: 管理者だけが実行できます
delete_user_safe: PermissionError: 管理者だけが実行できます
$ python -O permission.py
user-001 を削除しました(実行者: guest)
delete_user_safe: PermissionError: 管理者だけが実行できます

-O では、assert で守っていた delete_user が、管理者ではない guest のまま削除まで進んでいます。 if と raise で書いた delete_user_safe は、どちらの起動方法でも PermissionError で止まりました。

入力値の検証も同じです。フォームの値、ファイルの中身、APIの応答など、プログラムの外から来る値は、起動方法に関係なく必ず確かめる必要があるので、if と raise で書きます。例外の種類を ValueError、TypeError、PermissionError と使い分けられるので、呼び出す側も原因に応じた処理を書けます。

使ってよい場面・向かない場面の一覧

assert が向いているのは、「成り立たなかったら、それはコードの誤り」と言える条件です。向かないのは、「正しいコードでも、相手しだいで成り立たないことがある」条件です。

場面 assert 理由・代わりの書き方
関数の内部で、計算結果が想定の範囲にあることを確かめる 向いている 成り立たないのはコードの誤り
if / elif をすべて書いたあとの「ここには来ないはず」 向いている 分岐の書き漏れに気づける
開発中に、変数の型や形(リストの長さ、辞書のキー)を確かめる 向いている 前提をコードに残せる
ループの前後で変わらないはずの条件を確かめる 向いている アルゴリズムの確認になる
テストコードで結果を確かめる(pytest) 向いている pytest が詳しい差分を表示する
型チェッカーに「ここでは None ではない」と伝える 向いている assert x is not None
ユーザーが入力した値を確かめる 向かない if と raise ValueError
ファイル・APIなど外部から受け取ったデータを確かめる 向かない if と raise、または検証用ライブラリ
権限・ログイン状態を確かめる 向かない if と raise PermissionError
ファイルやキーが存在することを確かめる 向かない try / except で例外を受ける
公開する関数やライブラリの引数を確かめる 向かない if と raise TypeError / ValueError
保存・送信・取り出しなど、値を変える処理 向かない 処理は外に書き、結果だけを assert する
常に実行されてほしい確認 向かない if と raise(-O でも残る)

型チェッカーの行は、mypy などが assert x is not None のあとで型を絞り込むという一般に知られた仕様で、今回は型チェッカーを実行していません。

到達しないはずの分岐にはraise AssertionErrorも使える

「ここには来ないはず」を -O でも残したいときは、assert 文ではなく raise AssertionError(...) と書きます。これは普通の raise なので、最適化モードでも実行されます。Python 3.11 以降は typing.assert_never() も使えます。

import sys
from enum import Enum
from typing import assert_never


class Color(Enum):
    RED = 1
    BLUE = 2
    GREEN = 3


def label(color):
    if color is Color.RED:
        return "赤"
    if color is Color.BLUE:
        return "青"
    raise AssertionError(f"想定していない値: {color!r}")


def label2(color):
    if color is Color.RED:
        return "赤"
    if color is Color.BLUE:
        return "青"
    assert_never(color)


print("optimize =", sys.flags.optimize)
for func in (label, label2):
    try:
        print(func(Color.RED), func(Color.GREEN))
    except AssertionError as e:
        print(f"{func.__name__}: AssertionError: {e}")
$ python -O unreachable.py
optimize = 1
label: AssertionError: 想定していない値: <Color.GREEN: 3>
label2: AssertionError: Expected code to be unreachable, but got: <Color.GREEN: 3>

-O を付けても、どちらも AssertionError で止まっています。GREEN を追加したのに分岐を書き足していない、という書き漏れに実行時に気づけます。

assert・if+raise・pytest・unittestの使い分け

比べる点 assert 文 if と raise pytest の assert unittest の assertEqual
書く場所 アプリのコード アプリのコード テストコード テストコード
目的 コードの前提を確かめる 入力や状態を確かめ、呼び出し側へ伝える 結果が期待どおりかを確かめる 同左
-O での動き 実行されない 実行される テストファイル内は実行される(下の結果) 実行される
例外の種類 AssertionError のみ 自由に選べる AssertionError AssertionError
偽のときの表示 書いたメッセージ 書いたメッセージ 左右の値と差分を自動で表示 左右の値と差分を自動で表示
追加のインストール 不要 不要 必要(pip install pytest) 不要(標準ライブラリ)
呼び出し側が except で扱う 想定しない 想定する — —

pytestのassertは差分を表示する

テスト対象とテストコードを用意します。2つ目のテストは、期待値をわざと 221 にして、表示を確かめます。

def total(prices, tax=0.1):
    return round(sum(prices) * (1 + tax))
from cart import total


def test_total():
    assert total([100, 200]) == 330


def test_total_list():
    result = [total([100]), total([200])]
    assert result == [110, 221]
$ python -m pytest test_cart.py -q
.F                                                                       [100%]
=================================== FAILURES ===================================
_______________________________ test_total_list ________________________________

    def test_total_list():
        result = [total([100]), total([200])]
>       assert result == [110, 221]
E       assert [110, 220] == [110, 221]
E         
E         At index 1 diff: 220 != 221
E         Use -v to get more diff

test_cart.py:10: AssertionError
=========================== short test summary info ============================
FAILED test_cart.py::test_total_list - assert [110, 220] == [110, 221]
1 failed, 1 passed in 0.02s

(pytest 9.1.1 で実行。キャッシュ用フォルダを作らないよう -p no:cacheprovider を付けています。)

メッセージを書いていないのに、assert [110, 220] == [110, 221] と実際の値が展開され、「At index 1 diff: 220 != 221」と違う位置まで表示されています。 pytest はテストファイルを読み込むときに assert 文を書き換えて、この情報を集めています。そのため pytest では、assertEqual のような専用メソッドを覚えなくても、素の assert だけで十分な情報が得られます。

この書き換えのおかげで、python -O -m pytest と最適化モードで実行しても、テストファイルの中の assert は実行されました。

$ python -O -m pytest test_cart.py -q 2>&1 | tail -2
FAILED test_cart.py::test_total_list - assert [110, 220] == [110, 221]
1 failed, 1 passed, 1 warning in 0.02s

ただし、このとき pytest は PytestConfigWarning: assertions not in test modules or plugins will be ignored because assert statements are not executed by the underlying Python interpreter (are you using python -O?) という警告を出します。テストファイル以外(テスト対象のコードや補助モジュール)の assert は、-O では実行されないという意味です。テストは最適化なしで実行するのが基本です。

unittestのassertEqualと素のassert

同じ内容を unittest で書き、assertEqual と素の assert を並べます。

import unittest

from cart import total


class TotalTest(unittest.TestCase):
    def test_assert_equal(self):
        self.assertEqual([total([100]), total([200])], [110, 221])

    def test_plain_assert(self):
        assert [total([100]), total([200])] == [110, 221]
$ python -m unittest test_cart_unittest
FF
======================================================================
FAIL: test_assert_equal (test_cart_unittest.TotalTest.test_assert_equal)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_cart_unittest.py", line 8, in test_assert_equal
    self.assertEqual([total([100]), total([200])], [110, 221])
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Lists differ: [110, 220] != [110, 221]

First differing element 1:
220
221

- [110, 220]
?         ^

+ [110, 221]
?         ^


======================================================================
FAIL: test_plain_assert (test_cart_unittest.TotalTest.test_plain_assert)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_cart_unittest.py", line 11, in test_plain_assert
    assert [total([100]), total([200])] == [110, 221]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=2)

assertEqual は「Lists differ」と違いを詳しく表示しますが、素の assert は AssertionError とだけ表示され、実際の値がわかりません。 unittest は assert 文を書き換えないためです。さらに python -O -m unittest で実行すると、素の assert を使った test_plain_assert は何も確かめないまま通過し、結果は FAILED (failures=1) に変わりました(通常は failures=2)。unittest では self.assertEqual() などのメソッドを使うと、表示も動きも安定します。

unittest のメソッド 確かめる内容 pytest での書き方
assertEqual(a, b) a == b assert a == b
assertTrue(x) / assertFalse(x) 真・偽 assert x / assert not x
assertIs(a, b) / assertIsNone(x) 同じオブジェクト・None assert a is b / assert x is None
assertIn(a, b) a in b assert a in b
assertAlmostEqual(a, b) 小数の近さ assert a == pytest.approx(b)
assertRaises(Error) 例外が発生すること with pytest.raises(Error):

この対応表のうち、今回実行したのは assertEqual と assert a == b です。ほかの行は一般に知られた書き方として載せています。

assertの実行コスト

assert は条件を評価するだけなので、1回あたりの負担はわずかです。assert を2つ持つ関数を500万回呼んで、通常の実行と -O を比べました。

import sys
import timeit


def f(n):
    assert n >= 0
    assert isinstance(n, int)
    return n + 1


seconds = timeit.timeit("f(5)", globals=globals(), number=5_000_000)
print(f"optimize={sys.flags.optimize}: {seconds:.2f} 秒 / 500万回")
$ python cost.py
optimize=0: 0.26 秒 / 500万回
$ python -O cost.py
optimize=1: 0.14 秒 / 500万回

差は500万回で0.1〜0.2秒ほど、1回あたりにすると1億分の数秒です(数値は実行のたびに多少変わります)。速度のために assert を減らす必要はほとんどありません。 一方、assert sorted(data) == data や assert all(...) のように、条件の計算そのものが重い assert は、データが大きくなると時間がかかります。そうした確認は、テストコードの側へ移すと役割がはっきりします。

つまずきやすい点と直し方

症状 原因 直し方
条件が偽なのに assert を通過する assert(条件, "メッセージ") と括弧で囲んでいる 括弧を外して assert 条件, "メッセージ"
SyntaxWarning: assertion is always true, perhaps remove parentheses? 同上 同上。-W error で検出できる
手元では止まるのに、別の環境では止まらない -O または PYTHONOPTIMIZE が設定されている print(__debug__) で確かめる。必ず行いたい確認は if と raise にする
-O で結果が変わる assert の中に pop() や保存などの処理を書いている 処理を assert の外に出す
AssertionError とだけ出て原因がわからない メッセージを書いていない f-string で実際の値を入れる
値が 0 や空文字のときに止まる assert value と書いている assert value is not None と条件を明示する
SyntaxError: cannot assign to __debug__ __debug__ に代入している 起動方法(-O の有無)で切り替える
SyntaxError: invalid syntax lambda の中に assert を書いている(assert は文なので式の中には書けない) def で関数を定義する
unittest で実際の値が表示されない 素の assert を使っている self.assertEqual() などを使う

よくある質問

Q. assertとif+raiseはどう使い分けますか? A. 「成り立たなければコードの誤り」と言える前提には assert、「入力や外部の状態によっては成り立たないことがある」条件には if と raise を使います。assert は -O で消えるので、必ず実行したい確認には使いません。

Q. AssertionErrorはexceptで受け取ってよいですか? A. 文法上は except AssertionError: で受け取れます。ただ、assert は「起きないはずのこと」を知らせるものなので、受け取って処理を続けるよりも、止まった原因を直すのが基本です。呼び出し側に対応してもらいたい条件は、ValueError などを raise します。

Q. assertを無効化するにはどうすればよいですか? A. python -O ファイル名 で起動するか、環境変数 PYTHONOPTIMIZE に空でない値(1 など)を設定して起動します。プログラムの中から切り替える方法はありません。有効かどうかは __debug__ で確かめられます。

Q. 完成したコードからassertを取り除くべきですか? A. 取り除く必要はありません。前提を説明するコメントの役割も果たしますし、通常の起動では誤りに早く気づけます。大切なのは、assert が消えても動きが変わらないように書いておくことです。

まとめ

  • assert 条件, メッセージ は、条件が偽のときだけ AssertionError を発生させます。 メッセージには f-string で実際の値を入れます。
  • python -O と PYTHONOPTIMIZE で、assert は命令ごと消えます。 有効かどうかは __debug__ で確かめられます。
  • assert(条件, "メッセージ") はタプルと解釈されて常に真になります。 SyntaxWarning が出たら括弧を外します。
  • assert の中に pop() や保存などの処理を書くと、-O のときだけ結果が変わります。 処理は外に書き、assert は確認だけにします。
  • 入力検証と権限チェックは if と raise で書きます。assert はコードの前提を確かめる場面に使います。
  • pytest の assert は値と差分を自動で表示します。 unittest では self.assertEqual() などのメソッドを使います。
  • -O でも残したい「到達しないはずの分岐」は raise AssertionError(...) または typing.assert_never() を使います。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python assertの使い方|AssertionErrorと使いどころ」

株式会社RJCでは、Pythonのassertの使いどころと注意点を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?