- Python
Python assertの使い方|AssertionErrorと使いどころ
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期連続の黒字決算。