Argparse optional boolean [duplicate]

I am trying to get the following behaviour:

  • python test.py ⟹ store foo=False
  • python test.py --foo ⟹ store foo=True
  • python test.py --foo bool ⟹ store foo=bool

It works when I use

parser.add_argument('--foo', nargs='?', default=False, const=True)

However, it breaks if I add type=bool, trying to enforce casting to boolean. In this case

python test.py --foo False

Actually ends up storing foo=True. What's going on??


Are you sure you need that pattern? --foo and --foo <value>, together, for a boolean switch, is not a common pattern to use.

As for your issue, remember that the command line value is a string and, type=bool means that you want bool(entered-string-value) to be applied. For --foo False that means bool("False"), producing True; all non-empty strings are true! See Why is argparse not parsing my boolean flag correctly? as well.

Instead of supporting --foo / --foo <string value>, I would strongly recommend you use --foo to mean True, drop the argument value, and instead add a --no-foo option to explicitly set False:

parser.add_argument('--foo', default=False, action='store_true')
parser.add_argument('--no-foo', dest='foo', action='store_false')

The dest='foo' addition on the --no-foo switch ensures that the False value it stores (via store_false) ends up on the same args.foo attribute.

As of Python 3.9, you can also use the argparse.BooleanOptionalAction action class:

parser.add_argument("--foo", action=argparse.BooleanOptionalAction)

and it'll have the same effect, handling --foo and --no-foo to set and clear the flag.

You'd only need a --foo / --no-foo combination if you have some other configuration mechanism that would set foo to True and you needed to override this again with a command-line switch. --no-<option> is a widely adopted standard to invert a boolean command-line switch.

If you don't have a specific need for a --no-foo inverted switch (since just omitting --foo would already mean 'false'), then just stick with the action='store_true' option. This keeps your command line simple and clear!

However, if your use case or other constraints specifically require that your command line must have some king of --foo (true|false|0|1) support, then add your own converter:

def str_to_bool(value):
    if isinstance(value, bool):
        return value
    if value.lower() in {'false', 'f', '0', 'no', 'n'}:
        return False
    elif value.lower() in {'true', 't', '1', 'yes', 'y'}:
        return True
    raise ValueError(f'{value} is not a valid boolean value')

parser.add_argument('--foo', type=str_to_bool, nargs='?', const=True, default=False)
  • the const value is used for nargs='?' arguments where the argument value is omitted. Here that sets foo=True when --foo is used.
  • default=False is used when the switch is not used at all.
  • type=str_to_bool is used to handle the --foo <value> case.

Demo:

$ cat so52403065.py
from argparse import ArgumentParser

parser = ArgumentParser()

def str_to_bool(value):
    if value.lower() in {'false', 'f', '0', 'no', 'n'}:
        return False
    elif value.lower() in {'true', 't', '1', 'yes', 'y'}:
        return True
    raise ValueError(f'{value} is not a valid boolean value')

parser.add_argument('--foo', type=str_to_bool, nargs='?', const=True, default=False)

print(parser.parse_args())
$ python so52403065.py
Namespace(foo=False)
$ python so52403065.py --foo
Namespace(foo=True)
$ python so52403065.py --foo True
Namespace(foo=True)
$ python so52403065.py --foo no
Namespace(foo=False)
$ python so52403065.py --foo arrbuggrhellno
usage: so52403065.py [-h] [--foo [FOO]]
so52403065.py: error: argument --foo: invalid str_to_bool value: 'arrbuggrhellno'

You should use the action='store_true' parameter instead for Boolean arguments:

parser.add_argument('--foo', action='store_true')

So that the absence of the --foo option:

python test.py

would result in a False value for the foo argument, and the presence of the --foo option:

python test.py --foo

would result in a True value for the foo argument.