argparse type=bool Trap: The --force false That Force-Deletes Anyway2026-07-14
This script cleans a cache directory. The --force flag is meant to skip the confirmation prompt. Nightly CI runs it with --force false to get the safe, interactive behavior (no prompt gets answered, so nothing gets deleted). It worked in staging. In production, it wiped the cache every night for a week before anyone noticed.
import argparse
import shutil
import sys
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--force', type=bool, default=False,
help='Skip the confirmation prompt')
parser.add_argument('--cache-dir', default='/var/cache/myapp')
return parser.parse_args()
def main():
args = parse_args()
if args.force:
print(f"Force-deleting {args.cache_dir}")
shutil.rmtree(args.cache_dir)
else:
response = input(f"Delete {args.cache_dir}? [y/N]: ")
if response.strip().lower() == 'y':
shutil.rmtree(args.cache_dir)
else:
print("Cancelled.")
if __name__ == '__main__':
main()
The CI invocation: python clean_cache.py --force false --cache-dir /var/cache/prod. The engineer read that as "force is false, so prompt for confirmation." It read that way to the code reviewer, too.
argparse's type= is a callable that's applied to the raw string argument. When you write type=bool, argparse calls bool("false") on the incoming string. And bool() of a non-empty string is always True. Try it: bool("false"), bool("no"), bool("0"), bool("False") — every single one returns True. The only way to get False is to pass an empty string, which requires shell-escaping like --force "".
So --force false sets args.force = True, which takes the rmtree branch with no prompt. The cache vanishes. Worse, this looks correct to anyone who doesn't know the trap — the argument name, the value, and the flag semantics all read English-perfectly, and the code "does what it says" as far as the type system is concerned.
The staging cache existed but was empty and rebuildable, so nobody noticed the difference between "prompt cancelled" and "rmtree of nothing." Production had a 40GB cache that took two hours to rebuild.
Boolean CLI flags should almost never take a value. Use action='store_true' (presence means true), or on Python 3.9+, BooleanOptionalAction, which gives you both --force and --no-force for free:
parser.add_argument('--force', action=argparse.BooleanOptionalAction,
default=False, help='Skip the confirmation prompt')
Now --force sets it true, --no-force sets it false, and --force false raises an argument error instead of silently doing the opposite of what you meant.
If you truly need a value-taking boolean (config file compatibility, say), write the parser explicitly and reject anything ambiguous:
def strict_bool(s):
if s.lower() in ('true', '1', 'yes'): return True
if s.lower() in ('false', '0', 'no'): return False
raise argparse.ArgumentTypeError(f"expected boolean, got {s!r}")
parser.add_argument('--force', type=strict_bool, default=False)
The general rule: any time your type= callable would accept a string it shouldn't, you have a silent-coercion bug waiting to be triggered by the first person who trusts the flag name.
argparse's type=bool calls bool() on the argument string, and every non-empty string is truthy — so --force false silently sets force=True; use action='store_true' or BooleanOptionalAction for boolean flags.
