47

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??

Hyperplane
  • 1,422
  • 1
  • 14
  • 28

2 Answers2

91

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'
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • So the reason I want this is because the what I am writing is a builder file that writes a number of bash scripts and submits them to a SLURM workload manager. The optional parameters I submit get subjected to some preprocessing steps and then a derived set of parameters get appended to the python call inside the SLURM script. Now, the easiest way I found to implement this is to have a dictionary of parameters that I want to pass to the SLURM script and append all key-value pairs. Now with your suggestion I would have to check whether or not a flag was manually set or not. – Hyperplane Sep 19 '18 at 11:05
  • @Hyperplane: you could accept a JSON-encoded config string perhaps? Then if `args.json_config` is the decoded JSON object then all you have to do is use `args = parser.parse_args()` and `args.__dict__.update(args.json_config)` – Martijn Pieters Sep 19 '18 at 12:04
  • @Hyperplane: `parser.add_argument('--json-config', type=json.loads)` should be enough to define such a switch. – Martijn Pieters Sep 19 '18 at 12:05
  • That sounds like a really nice solution. At the moment using a `type=string2bool` seems sufficient, but I will keep this idea in mind. Thanks again. – Hyperplane Sep 19 '18 at 12:20
  • for the --no- pattern argparse introduced the BooleanOptionalAction https://docs.python.org/3/library/argparse.html#action – pseyfert Nov 23 '20 at 10:03
  • @pseyfert: gotta love the lack of proper documentation on that addition, or even a 'added in' entry! So it is [new in 3.9](https://bugs.python.org/issue8538). – Martijn Pieters Nov 23 '20 at 16:43
29

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.

blhsing
  • 91,368
  • 6
  • 71
  • 106