この記事について この記事は、生成AIを活用した自動生成フローで作成しています。Python公式のargparseと真偽値の仕様を確認し、標準ライブラリだけで引数変換の落とし穴を再現しました。
検証ステータス:🧪 Pythonローカル実行で論理確認。Windows・macOSと実運用ジョブへの適用は未検証
Pythonの–dry-run FalseがTrueになる?argparseのbool指定を安全に直す
Pythonのコマンドラインツールに「確認だけで変更しない」という意味の--dry-runを用意したつもりなのに、--dry-run Falseと入力してもTrueになってしまう。原因は、argparseのtype=boolが「Falseという単語を偽へ変換する機能」ではないためです。
この勘違いは単なる表示の問題ではありません。ファイルの上書きを許可するか、通知を送るか、削除を実行するか、といった重要なフラグに同じ書き方をすると、実行者が考えた状態とプログラムの状態が逆転する危険があります。本記事では、ファイルや外部サービスを一切変更しない短いコードで現象を再現し、誤解しにくい引数設計に直します。
対象環境と今回の確認事項
| 項目 | 今回の条件 |
|---|---|
| 利用するもの | Python 3の標準ライブラリargparse |
| 比べる方式 | type=bool、store_true、BooleanOptionalAction |
| バージョン注意 | BooleanOptionalActionはPython 3.9以降 |
| 安全な範囲 | Pythonの文字列と画面出力だけ。ファイル作成・削除・通信なし |
| 確認したい結果 | Falseという文字からTrueが作られる理由を説明できる |
運用スクリプトでは、対象ファイルの一覧だけを表示する試運転と、実際に変更する本実行を切り替えることがあります。ここで失敗すると困るのは、確認モードを指定したつもりが実際には解除されてしまうことです。本記事のコードはその切替えの値を印字するだけなので、どのケースを実行しても実ファイルには触れません。
まず誤った指定を再現する
次のコードは、type=boolを指定した例です。parse_args()には引数の配列を直接渡せるため、ターミナルのコマンドを何度も変更しなくても同じ条件を安全に比較できます。先頭の空配列は、オプションをまったく指定しなかった場合です。
import argparse
parser = argparse.ArgumentParser(prog="sample")
parser.add_argument("--dry-run", type=bool, default=False)
for supplied in (
[],
["--dry-run", "False"],
["--dry-run", "0"],
["--dry-run", ""],
):
result = parser.parse_args(supplied)
print(repr(supplied), "=>", result.dry_run)
実行すると、空配列はFalse、文字列の"False"も"0"もTrueになります。意外なことに、["--dry-run", ""]のような空文字列だけはFalseです。つまり、人間が「偽を表す」と考える単語を並べ替えても直りません。小文字のfalseやnoへ置き換えても、文字列が空でなければ真になります。
ここで利用者が実行したかったのが--dry-run Falseで試運転を解除することなら、実際には試運転が有効です。一方、--delete Falseや--allow-write Falseという危険な操作の許可を同じ方式で処理していると、「許可していないつもり」が許可へ反転する恐れもあります。どちらも設計上の事故です。
なぜFalseが真になるのか
Pythonの組込みbool()は、文字列の英語の意味を解釈しません。文字列が空かどうかを評価します。bool("")は偽ですが、bool("False")、bool("0")、bool("no")はいずれも真です。中身に一文字でも入っていれば、一般的には空でない文字列として真になります。
argparseのtype=は、指定された変換関数へ入力文字列を渡す仕組みです。したがってtype=boolは、Falseという語を事前登録された値として読むのではなく、bool("False")を呼び出すのと同じ結果になります。これはargparseのバグではなく、bool()の仕様と引数変換の仕組みを組み合わせた結果です。
この原理を知っておくと、なぜ--dry-run 0でも直らないかを説明できます。数値の0をbool(0)へ渡せば偽ですが、コマンドラインの"0"はまず文字列として渡ります。ここで型が異なるのです。似た見た目の値でも、プログラムが受け取った型を確認しなければ誤った結論になります。
正しい設計その1:フラグの有無だけを見る
例えば詳細ログを出すための--verboseなら、action="store_true"が簡潔です。オプションが存在したら真、指定がなければ偽として扱います。値を後ろに書く必要はありません。--verbose Falseと入力すると、Falseは不要な引数として拒否されるため、曖昧な解釈を避けられます。
ただし、フラグを無効化したいときの操作方法も利用者に伝える必要があります。もともと既定値が偽なら、オプションを付けないことが無効化です。既定値を真にして使う仕組みでは、フラグがあるかないかだけだと「明示的に偽にしたい」という要求を表しにくくなります。
重要なのは、store_trueが常に最も安全ということではありません。オプションが何を許可し、何を禁止するのかを決めたうえで、明確な操作を設計することです。
正しい設計その2:肯定形と否定形を用意する
Python 3.9以降ではargparse.BooleanOptionalActionを使えます。これを使うと、--dry-runと--no-dry-runという二つの形を用意できます。文字列の"False"を読んで変換する必要がなく、オプションの名前で意図を表せます。
次は、何も指定しない限り確認モードが有効になる例です。動作は画面に表示するだけで、実際の書込み処理は含みません。
import argparse
parser = argparse.ArgumentParser(prog="sample")
parser.add_argument(
"--dry-run",
action=argparse.BooleanOptionalAction,
default=True,
help="変更しない確認モード。既定は有効",
)
for supplied in ([], ["--dry-run"], ["--no-dry-run"]):
result = parser.parse_args(supplied)
print(repr(supplied), "=>", result.dry_run)
このコードでは、指定なしと--dry-runがTrue、--no-dry-runがFalseになります。実行者は試運転の解除を--no-dry-runという明確なオプションで指定できます。ただし、本番の削除や送信を許可するツールなら、確認モードを解除できるだけで操作を開始してよいわけではありません。対象IDや件数、権限、必要な承認を別に確かめる設計が必要です。
図で見る:何を真偽値とみなしているのか
下の図は、type=boolと肯定・否定フラグの違いを示します。左側は入力文字列がbool()へ送られます。右側はオプション名そのものが真偽値を決めます。文字列の単語と、オプションの意味を混同しないでください。
flowchart TD
A[引数を受け取る] --> B{指定方法を選ぶ}
B -- type=bool --> C[入力文字列をboolへ渡す]
C --> D[空でない文字列はTrue]
B -- BooleanOptionalAction --> E{オプション名を確認}
E -- 肯定形 --> F[True]
E -- 否定形 --> G[False]
type=boolでFalseという見た目を入力した場合は、結局「空でない文字列」を渡しています。対して--no-dry-runは「確認モードを無効化したい」と意思表示する名前です。図が示しているのは、どちらが技術的に上位という話ではなく、ユーザーの入力意図を失わない設計の違いです。
条件を変える:既定値が安全とは限らない
上の例はdefault=Trueです。確認モードを何も指定しないときの既定にしているので、利用者がオプションを書き忘れても変更処理へ進みにくくなります。一方、--verboseのような詳細表示のフラグは、既定を偽として必要なときだけ有効にするのが自然です。
もし--deleteが真のときに削除を行う設計なら、既定値は偽が適切でしょう。つまり、真だから安全、偽だから危険という一般則はありません。フラグ名が表す意味と、プログラムの分岐をセットにして確認する必要があります。名前だけ--dry-runにしても、内部条件が逆なら同じ事故が起こります。
実際の業務では、確認モードの解除を許す条件、対象範囲、エラー時の戻し方なども設計します。ある一回のパース結果が正しいことと、コマンド全体が安全であることは別です。本番に組み込む前に、通常実行と否定形指定の両方をダミーデータで確かめてください。
失敗時の切り分け:三つの場所を調べる
まず、repr()などでプログラムが受け取った引数の文字列を確認します。シェル側の引用符や環境変数の展開によって、思ったものと別の文字列が渡る場合があります。次にargparseのパース結果の型と値を確認します。最後に、その真偽値を読んでいる処理の分岐を調べます。この三つを区別しなければ、引数変換の誤りと、削除や送信処理の実装ミスを取り違えます。
BooleanOptionalActionが存在しないと表示される場合、Python 3.9未満かどうかを確認してください。その場合はstore_trueとstore_falseを別のオプションに付け、同じ宛先変数へ格納する方法があります。ただし両方を同時に指定した場合の優先順位を決め、ヘルプに明示しなければ別の曖昧さを生むので注意が必要です。
--dry-run Falseが余計な引数としてエラーになる場合は、store_trueやBooleanOptionalActionが値なしフラグとして動いている可能性があります。このときは文字列のFalseを付けず、必要なら否定形--no-dry-runを使います。「エラーが出なくなったこと」だけで合格にせず、期待した真偽値になったかまで確認します。
手順書と監視ログをどう見直すか
CLIを運用するチームでは、引数の具体例が手順書やジョブ定義に記録されます。実装だけ直して--dry-run Falseという古い例を残すと、次の担当者が同じ誤解をします。まずダミー入力で再現し、修正後の正しいオプションを書き直し、その引数で変わる動作を説明します。
ログには、秘密情報や個人情報を出さず、実際に解釈された確認モードの値を必要な範囲で記録すると原因調査が容易になります。特に危険な処理を含む場合、何を操作対象とする予定だったかを変更前に表示できると、設定ミスの早期発見に役立ちます。ただし、ログを書いたことで操作の安全性が保証されるわけではありません。
運用上の結論は、真偽値を扱うコマンドにtype=boolを安易に使わず、利用者の操作意図を表すフラグの有無や肯定・否定形を設計することです。 正しい値が入ることを実験で確かめ、業務上の安全策は別の層でも維持します。
公式資料と検証範囲
Python公式ドキュメント:argparse —
store_true、BooleanOptionalAction、parse_argsの仕様Python公式ドキュメント:真偽値の評価 — 空文字列・非空文字列の真偽値
今回のローカル検証ではPythonで文字列Falseと0の変換、および肯定・否定・省略を実行しました。実ファイルを上書きするCLIや実際の運用ジョブに適用したわけではありません。
