SuperElectric
SuperElectric

Reputation: 18914

Parsing boolean values with argparse

I would like to use argparse to parse boolean command-line arguments written as "--foo True" or "--foo False". For example:

my_program --my_boolean_flag False

However, the following test code does not do what I would like:

import argparse
parser = argparse.ArgumentParser(description="My parser")
parser.add_argument("--my_bool", type=bool)
cmd_line = ["--my_bool", "False"]
parsed_args = parser.parse(cmd_line)

Sadly, parsed_args.my_bool evaluates to True. This is the case even when I change cmd_line to be ["--my_bool", ""], which is surprising, since bool("") evalutates to False.

How can I get argparse to parse "False", "F", and their lower-case variants to be False?

Upvotes: 1155

Views: 988054

Answers (25)

Wanderer
Wanderer

Reputation: 1197

The issue arises because type=bool in argparse doesn't work as expected. Any non-empty string (like "False" OR 1 OR abcd123) is evaluated as True in Python.

Option 1: Custom Function

Use a custom function to handle True and False values explicitly:

import argparse

def str2bool(value):
    if isinstance(value, bool):
        return value
    if value.lower() in {'true', 't', 'yes', '1'}:
        return True
    elif value.lower() in {'false', 'f', 'no', '0'}:
        return False
    else:
        raise argparse.ArgumentTypeError(f'Invalid boolean value: {value}')

parser = argparse.ArgumentParser(description="My parser")
parser.add_argument("--my_bool", type=str2bool, default=False, help="Boolean flag")
cmd_line = ["--my_bool", "False"]
parsed_args = parser.parse_args(cmd_line)

print(parsed_args.my_bool)  # Output: False

This would run as

python my_program.py --my_bool True
# Output: True

python my_program.py --my_bool False
# Output: False

Option 2: Use store_true or store_false - you will find this more commonly

parser = argparse.ArgumentParser(description="My parser")
parser.add_argument("--my_bool", action="store_true", help="Set flag to True")
parser.add_argument("--no_bool", action="store_false", dest="my_bool", help="Set flag to False")
cmd_line = ["--my_bool"]
parsed_args = parser.parse_args(cmd_line)

print(parsed_args.my_bool)  # Output: True

This would run as:

python my_program.py --my_bool
# Output: True

python my_program.py --no_bool
# Output: False

not sure about the performance though

Upvotes: 0

Russell
Russell

Reputation: 1442

Simple and safe.

from ast import literal_eval

parser.add_argument('--boolean_flag',
                      help='This is a boolean flag.',
                      type=literal_eval, 
                      choices=[True, False], 
                      default=True)

EDIT several people have commented that it is not safe to use the eval built-in. This was noted in a foot-note. But given that folks will sometimes copy-paste from SO, better to have the safer, more correct answer.

Upvotes: 16

Moe
Moe

Reputation: 187

Understanding argparse and Boolean Arguments

When using argparse to handle boolean command-line arguments, specifying type=bool doesn't behave as you might expect. The type=bool argument will always evaluate the provided value as True, regardless of whether you pass True or False.

For example:

parser.add_argument("--my_bool", type=bool, default=False)

When you run:

my_program --my_bool False

arg.my_bool will be set to True, not False, because any non-empty string argument (including "False") is interpreted as True by Python's bool() function.

Correct Way to Handle Boolean Flags

To correctly handle boolean flags, use the action parameter with store_true or store_false. This way, the presence of the flag sets the value to True, and its absence sets the value to False.

For example:

parser.add_argument("--my_boolean_flag", action="store_true", help="Set this flag to enable feature X")
  • If --my_boolean_flag is provided on the command line, arg.my_boolean_flag will be True.
  • If --my_boolean_flag is not provided, arg.my_boolean_flag will be False.

Example Usage

Here’s a complete example for clarity:

import argparse

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("--my_boolean_flag", action="store_true"(argparse.BooleanOptionalAction), help="Enable the boolean flag")
    args = parser.parse_args()

    print(args.my_boolean_flag)
  • Running my_program will output False.
  • Running my_program --my_boolean_flag will output True.

Upvotes: 4

Peng Zheng
Peng Zheng

Reputation: 77

Actually, very easy. No packages need to be imported.

Remember that type=func means returning the func(x), like type=bool -> bool(x), so:

parser.add_argument('--feature', default=False, type=lambda x: x == 'True')

Now that we have known the principle, you can also extend it to type=lambda x: x.lower() not in ['false', 'no', '0', 'none', ...].

Upvotes: 3

reddy nishanth
reddy nishanth

Reputation: 454

You can create a BoolAction and then use it

class BoolAction(Action):
    def __init__(
            self,
            option_strings,
            dest,
            nargs=None,
            default: bool = False,
            **kwargs,
    ):
        if nargs is not None:
            raise ValueError('nargs not allowed')
        super().__init__(option_strings, dest, default=default, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        input_value = values.lower()
        b = input_value in ['true', 'yes', '1']
        if not b and input_value not in ['false', 'no', '0']:
            raise ValueError('Invalid boolean value "%s".)
        setattr(namespace, self.dest, b)

and then set action=BoolAction in parser.add_argument()

Upvotes: -1

mgilson
mgilson

Reputation: 310117

I think a more canonical way to do this is via:

command --feature

and

command --no-feature

argparse supports this version nicely:

Python 3.9+:

parser.add_argument('--feature', action=argparse.BooleanOptionalAction)

Python < 3.9:

parser.add_argument('--feature', action='store_true')
parser.add_argument('--no-feature', dest='feature', action='store_false')
parser.set_defaults(feature=True)

Of course, if you really want the --arg <True|False> version, you could pass ast.literal_eval as the "type", or a user defined function ...

def t_or_f(arg):
    ua = str(arg).upper()
    if 'TRUE'.startswith(ua):
       return True
    elif 'FALSE'.startswith(ua):
       return False
    else:
       pass  #error condition maybe?

Upvotes: 1562

FiddleStix
FiddleStix

Reputation: 3731

Expanding on gerardw's answer

The reason parser.add_argument("--my_bool", type=bool) doesn't work is that bool("mystring") is True for any non-empty string so bool("False") is actually True.

What you want is

my_program.py

import argparse

parser = argparse.ArgumentParser(description="My parser")

parser.add_argument(
        "--my_bool",
        choices=["False", "True"],
        )

parsed_args = parser.parse_args()

my_bool = parsed_args.my_bool == "True"

print(my_bool)
$ python my_program.py --my_bool False
False

$ python my_program.py --my_bool True
True

$ python my_program.py --my_bool true
usage: my_program.py [-h] [--my_bool {False,True}]
my_program.py: error: argument --my_bool: invalid choice: 'true' (choose from 'False', 'True')

Upvotes: 3

Zach Rieck
Zach Rieck

Reputation: 449

This is actually outdated. For Python 3.7+, Argparse now supports boolean args (search BooleanOptionalAction).

The implementation looks like this:

import argparse

ap = argparse.ArgumentParser()

# List of args
ap.add_argument('--foo', default=True, type=bool, help='Some helpful text that is not bar. Default = True')

# Importable object
args = ap.parse_args()

One other thing to mention: this will block all entries other than True and False for the argument via argparse.ArgumentTypeError. You can create a custom error class for this if you want to try to change this for any reason.

Upvotes: 8

Maxim
Maxim

Reputation: 7705

Yet another solution using the previous suggestions, but with the "correct" parse error from argparse:

def str2bool(v):
    if isinstance(v, bool):
        return v
    if v.lower() in ('yes', 'true', 't', 'y', '1'):
        return True
    elif v.lower() in ('no', 'false', 'f', 'n', '0'):
        return False
    else:
        raise argparse.ArgumentTypeError('Boolean value expected.')

This is very useful to make switches with default values; for instance

parser.add_argument("--nice", type=str2bool, nargs='?',
                        const=True, default=False,
                        help="Activate nice mode.")

allows me to use:

script --nice
script --nice <bool>

and still use a default value (specific to the user settings). One (indirectly related) downside with that approach is that the 'nargs' might catch a positional argument -- see this related question and this argparse bug report.

Upvotes: 522

Indra Ginanjar
Indra Ginanjar

Reputation: 366

After previously following @akash-desarda 's excellence answer https://stackoverflow.com/a/59579733/315112 , to use strtobool via lambda, later, I decide to use strtobool directly instead.

import argparse
from distutils import util
parser.add_argument('--feature', type=util.strtobool)

Yes you're right, strtobool is returning an int, not a bool. But strtobool will not returning any other value except 0 and 1, and python will get them converted to a bool value seamlessy and consistently.

>>> 0 == False
True
>>> 0 == True
False
>>> 1 == False
False
>>> 1 == True
True

While on receiving a wrong input value like

python yours.py --feature wrong_value

An argparse.Action with strtobool compared to lambda will produce a slightly clearer/comprehensible error message:

yours.py: error: argument --feature: invalid strtobool value: 'wrong_value'

Compared to this code,

parser.add_argument('--feature', type=lambda x: bool(util.strtobool(x))

Which will produce a less clear error message:

yours.py: error: argument --feature: invalid <lambda> value: 'wrong_value'

Upvotes: 3

scorpionipx
scorpionipx

Reputation: 105

Convert the value:

def __arg_to_bool__(arg):
    """__arg_to_bool__

        Convert string / int arg to bool
    :param arg: argument to be converted
    :type arg: str or int
    :return: converted arg
    :rtype: bool
    """
    str_true_values = (
        '1',
        'ENABLED',
        'ON',
        'TRUE',
        'YES',
    )
    str_false_values = (
        '0',
        'DISABLED',
        'OFF',
        'FALSE',
        'NO',
    )

    if isinstance(arg, str):
        arg = arg.upper()
        if arg in str_true_values:
            return True
        elif arg in str_false_values:
            return False

    if isinstance(arg, int):
        if arg == 1:
            return True
        elif arg == 0:
            return False

    if isinstance(arg, bool):
        return arg

    # if any other value not covered above, consider argument as False
    # or you could just raise and error
    return False

[...]


args = ap.parse_args()
my_arg = options.my_arg
my_arg = __arg_to_bool__(my_arg)

Upvotes: -1

Akash Desarda
Akash Desarda

Reputation: 868

Simplest & most correct way is:

from distutils.util import strtobool

parser.add_argument('--feature', dest='feature', 
                    type=lambda x: bool(strtobool(x)))

Do note that True values are y, yes, t, true, on and 1; false values are n, no, f, false, off and 0. Raises ValueError if val is anything else.

Upvotes: 45

Schaki
Schaki

Reputation: 1791

Here is another variation without extra row/s to set default values. The boolean value is always assigned, so that it can be used in logical statements without checking beforehand:

import argparse
parser = argparse.ArgumentParser(description="Parse bool")
parser.add_argument("--do-something", default=False, action="store_true",
                    help="Flag to do something")
args = parser.parse_args()

if args.do_something:
     print("Do something")
else:
     print("Don't do something")

print(f"Check that args.do_something={args.do_something} is always a bool.")

Upvotes: 131

fnkr
fnkr

Reputation: 10075

If you want to allow --feature and --no-feature at the same time (last one wins)

This allows users to make a shell alias with --feature, and overriding it with --no-feature.

Python 3.9 and above

parser.add_argument('--feature', default=True, action=argparse.BooleanOptionalAction)

Python 3.8 and below

I recommend mgilson's answer:

parser.add_argument('--feature', dest='feature', action='store_true')
parser.add_argument('--no-feature', dest='feature', action='store_false')
parser.set_defaults(feature=True)

If you DON'T want to allow --feature and --no-feature at the same time

You can use a mutually exclusive group:

feature_parser = parser.add_mutually_exclusive_group(required=False)
feature_parser.add_argument('--feature', dest='feature', action='store_true')
feature_parser.add_argument('--no-feature', dest='feature', action='store_false')
parser.set_defaults(feature=True)

You can use this helper if you are going to set many of them:

def add_bool_arg(parser, name, default=False):
    group = parser.add_mutually_exclusive_group(required=False)
    group.add_argument('--' + name, dest=name, action='store_true')
    group.add_argument('--no-' + name, dest=name, action='store_false')
    parser.set_defaults(**{name:default})

add_bool_arg(parser, 'useful-feature')
add_bool_arg(parser, 'even-more-useful-feature')

Upvotes: 391

FEMengineer
FEMengineer

Reputation: 19

Quick and easy, but only for arguments 0 or 1:

parser.add_argument("mybool", default=True,type=lambda x: bool(int(x)))
myargs=parser.parse_args()
print(myargs.mybool)

The output will be "False" after calling from terminal:

python myscript.py 0

Upvotes: 1

gerardw
gerardw

Reputation: 6329

Simplest way would be to use choices:

parser = argparse.ArgumentParser()
parser.add_argument('--my-flag',choices=('True','False'))

args = parser.parse_args()
flag = args.my_flag == 'True'
print(flag)

Not passing --my-flag evaluates to False. The required=True option could be added if you always want the user to explicitly specify a choice.

Upvotes: 2

Evalds Urtans
Evalds Urtans

Reputation: 6704

oneliner:

parser.add_argument('--is_debug', default=False, type=lambda x: (str(x).lower() == 'true'))

Upvotes: 97

4nu81
4nu81

Reputation: 27

I think the most canonical way will be:

parser.add_argument('--ensure', nargs='*', default=None)

ENSURE = config.ensure is None

Upvotes: 1

arunkumarreddy
arunkumarreddy

Reputation: 177

A simpler way would be to use as below.

parser.add_argument('--feature', type=lambda s: s.lower() in ['true', 't', 'yes', '1'])

Upvotes: 10

Stumpy Joe Pete
Stumpy Joe Pete

Reputation: 356

This works for everything I expect it to:

add_boolean_argument(parser, 'foo', default=True)
parser.parse_args([])                   # Whatever the default was
parser.parse_args(['--foo'])            # True
parser.parse_args(['--nofoo'])          # False
parser.parse_args(['--foo=true'])       # True
parser.parse_args(['--foo=false'])      # False
parser.parse_args(['--foo', '--nofoo']) # Error

The code:

def _str_to_bool(s):
    """Convert string to bool (in argparse context)."""
    if s.lower() not in ['true', 'false']:
        raise ValueError('Need bool; got %r' % s)
    return {'true': True, 'false': False}[s.lower()]

def add_boolean_argument(parser, name, default=False):                                                                                               
    """Add a boolean argument to an ArgumentParser instance."""
    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        '--' + name, nargs='?', default=default, const=True, type=_str_to_bool)
    group.add_argument('--no' + name, dest=name, action='store_false')

Upvotes: 13

dl.meteo
dl.meteo

Reputation: 1786

A quite similar way is to use:

feature.add_argument('--feature',action='store_true')

and if you set the argument --feature in your command

 command --feature

the argument will be True, if you do not set type --feature the arguments default is always False!

Upvotes: 33

Robert T. McGibbon
Robert T. McGibbon

Reputation: 5287

class FlagAction(argparse.Action):
    # From http://bugs.python.org/issue8538

    def __init__(self, option_strings, dest, default=None,
                 required=False, help=None, metavar=None,
                 positive_prefixes=['--'], negative_prefixes=['--no-']):
        self.positive_strings = set()
        self.negative_strings = set()
        for string in option_strings:
            assert re.match(r'--[A-z]+', string)
            suffix = string[2:]
            for positive_prefix in positive_prefixes:
                self.positive_strings.add(positive_prefix + suffix)
            for negative_prefix in negative_prefixes:
                self.negative_strings.add(negative_prefix + suffix)
        strings = list(self.positive_strings | self.negative_strings)
        super(FlagAction, self).__init__(option_strings=strings, dest=dest,
                                         nargs=0, const=None, default=default, type=bool, choices=None,
                                         required=required, help=help, metavar=metavar)

    def __call__(self, parser, namespace, values, option_string=None):
        if option_string in self.positive_strings:
            setattr(namespace, self.dest, True)
        else:
            setattr(namespace, self.dest, False)

Upvotes: 0

hpaulj
hpaulj

Reputation: 231615

There seems to be some confusion as to what type=bool and type='bool' might mean. Should one (or both) mean 'run the function bool(), or 'return a boolean'? As it stands type='bool' means nothing. add_argument gives a 'bool' is not callable error, same as if you used type='foobar', or type='int'.

But argparse does have registry that lets you define keywords like this. It is mostly used for action, e.g. `action='store_true'. You can see the registered keywords with:

parser._registries

which displays a dictionary

{'action': {None: argparse._StoreAction,
  'append': argparse._AppendAction,
  'append_const': argparse._AppendConstAction,
...
 'type': {None: <function argparse.identity>}}

There are lots of actions defined, but only one type, the default one, argparse.identity.

This code defines a 'bool' keyword:

def str2bool(v):
  #susendberg's function
  return v.lower() in ("yes", "true", "t", "1")
p = argparse.ArgumentParser()
p.register('type','bool',str2bool) # add type keyword to registries
p.add_argument('-b',type='bool')  # do not use 'type=bool'
# p.add_argument('-b',type=str2bool) # works just as well
p.parse_args('-b false'.split())
Namespace(b=False)

parser.register() is not documented, but also not hidden. For the most part the programmer does not need to know about it because type and action take function and class values. There are lots of stackoverflow examples of defining custom values for both.


In case it isn't obvious from the previous discussion, bool() does not mean 'parse a string'. From the Python documentation:

bool(x): Convert a value to a Boolean, using the standard truth testing procedure.

Contrast this with

int(x): Convert a number or string x to an integer.

Upvotes: 41

foo
foo

Reputation: 1018

In addition to what @mgilson said, it should be noted that there's also a ArgumentParser.add_mutually_exclusive_group(required=False) method that would make it trivial to enforce that --flag and --no-flag aren't used at the same time.

Upvotes: 12

susundberg
susundberg

Reputation: 727

I was looking for the same issue, and imho the pretty solution is :

def str2bool(v):
  return v.lower() in ("yes", "true", "t", "1")

and using that to parse the string to boolean as suggested above.

Upvotes: 20

Related Questions