Implementing a flag option

39 views
Skip to first unread message

doerw...@googlemail.com

unread,
Jan 19, 2011, 10:16:47 AM1/19/11
to argparse-users
I'm trying to implement a flag option that behaves like this: The
option has a default (bool) value. When I specify the option without a
value I get the negated default. When I specify an option value 'yes'
or 'on' I get True, and when I specify 'no' or 'off' I get False, i.e.
for the option:

p.add_argument("-v", "--verbose",
dest="verbose",
help="Be verbose? (default: %(default)s)",
default=False)

I want to get:

foo.py --> args.verbose == False
foo.py -v --> args.verbose == True
foo.py -v=no --> args.verbose == False
foo.py -v=yes --> args.verbose == True

What I tried was this:

def flag(value):
value = value.lower()
if value in ("1", "true", "yes", "on"):
return True
elif value in ("0", "false", "no", "off"):
return False
raise ValueError("unknown flag value")

p.add_argument("-v", "--verbose", dest="verbose",
help="Be verbose? (default: %(default)s)",
default=False,
type=flag, nargs="?", const=True)

The problem with this approach is that the help message then states:

-v [VERBOSE], --verbose [VERBOSE]
Be verbose? (default: False)

instead of:

-v [VERBOSE], --verbose [VERBOSE]
Be verbose? (default: no)

My second try was this:

class FlagAction(argparse.Action):
true_choices = ("1", "true", "yes", "on")
false_choices = ("0", "false", "no", "off")

def __init__(self, option_strings, dest, default=False, help=None):
super(FlagAction, self).__init__(
option_strings=option_strings, dest=dest,
default="yes" if default else "no", help=help, metavar="yes|
no",
const="no" if default else "yes", nargs="?")

def __call__(self, parser, namespace, values, option_string=None):
values = values.lower()
if values in self.true_choices:
values = True
elif values in self.false_choices:
values = False
elif not values:
values = self.default != "yes"
else:
parser.error("argument {}: invalid flag value: {!r}".format(
"/".join(self.option_strings), values)
setattr(namespace, self.dest, values)

p.add_argument("-v", "--verbose", dest="verbose",
help="Be verbose? (default: %(default)s)",
default=False,
action=FlagAction)


However the problem with this approach is that when the option isn't
specified I get args.verbose == "no" instead of args.verbose == "yes".

Is there a way out of this problem? It seems like I could use the
first approach and somehow overwrite the help message formatting
logic, but the documentation states that all Formatter methods are off
limits.

Servus,
Walter

doerw...@googlemail.com

unread,
Jan 19, 2011, 10:57:52 AM1/19/11
to argparse-users
On 19 Jan., 16:16, "doerwal...@googlemail.com"
<doerwal...@googlemail.com> wrote:

Oops, the following:

> However the problem with this approach is that when the option isn't
> specified I get args.verbose == "no" instead of args.verbose == "yes".

was supposed to read:

However the problem with this approach is that when the option isn't
specified I get args.verbose == "no" instead of args.verbose == False.

Servus,
Walter

Steven Bethard

unread,
Jan 20, 2011, 8:54:39 AM1/20/11
to argpars...@googlegroups.com

This looks reasonable, except that I don't see why you do
`default="yes" if default else "no"` instead of just
`default=default`. I think if you drop that, then you'll get
args.verbose == False when the option isn't specified.

Steve
--
Where did you get that preposterous hypothesis?
Did Steve tell you that?
        --- The Hiphopopotamus

doerw...@googlemail.com

unread,
Jan 21, 2011, 11:28:36 AM1/21/11
to argparse-users
On 20 Jan., 14:54, Steven Bethard <steven.beth...@gmail.com> wrote:
> On Wed, Jan 19, 2011 at 4:16 PM, doerwal...@googlemail.com
True, but then a %(default)s in the help message will output True/
False again.

Servus,
Walter

Steven Bethard

unread,
Jan 23, 2011, 5:21:20 AM1/23/11
to argpars...@googlegroups.com
On Fri, Jan 21, 2011 at 5:28 PM, doerw...@googlemail.com
<doerw...@googlemail.com> wrote

> True, but then a %(default)s in the help message will output True/
> False again.

Ahh, I see. I think you want to stick with your original "flag" type
approach, and then specify nargs='?', const='no' and default='no':

def flag(value):
value = value.lower()

if value in ("1", "true", "yes", "on"):
return True
elif value in ("0", "false", "no", "off"):


return False
raise ValueError("unknown flag value")

parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", dest="verbose",


help="Be verbose? (default: %(default)s)",

nargs='?', type=flag,
const="no", default="no")

The nargs='?' says that the argument to '-v' is optional, the
default='no' says what to use when no '-v' is present, and the
const='no' says what to use when '-v' is present but there's no
argument. With this approach, I see:

$ python temp.py -h
usage: temp.py [-h] [-v [VERBOSE]]

optional arguments:
-h, --help show this help message and exit


-v [VERBOSE], --verbose [VERBOSE]
Be verbose? (default: no)

$ python temp.py
Namespace(verbose=False)
$ python temp.py -v
Namespace(verbose=False)
$ python temp.py -v yes
Namespace(verbose=True)

I'm not sure if you also wanted something instead of "VERBOSE". If you
do, you can add something like metavar="yes|no".

Steven Bethard

unread,
Jan 23, 2011, 5:23:27 AM1/23/11
to argpars...@googlegroups.com
On Sun, Jan 23, 2011 at 11:21 AM, Steven Bethard
<steven....@gmail.com> wrote:
> On Fri, Jan 21, 2011 at 5:28 PM, doerw...@googlemail.com
> <doerw...@googlemail.com> wrote
>> True, but then a %(default)s in the help message will output True/
>> False again.
>
> Ahh, I see. I think you want to stick with your original "flag" type
> approach, and then specify nargs='?', const='no' and default='no':
>
> def flag(value):
>    value = value.lower()
>    if value in ("1", "true", "yes", "on"):
>        return True
>    elif value in ("0", "false", "no", "off"):
>        return False
>    raise ValueError("unknown flag value")
>
> parser = argparse.ArgumentParser()
> parser.add_argument("-v", "--verbose", dest="verbose",
>                    help="Be verbose? (default: %(default)s)",
>                    nargs='?', type=flag,
>                    const="no", default="no")
>
> The nargs='?' says that the argument to '-v' is optional, the
> default='no' says what to use when no '-v' is present, and the
> const='no' says what to use when '-v' is present but there's no
> argument.

Hmm... On second thought I think maybe you actually want const='yes'
here. But you can play around with it a bit and see.

doerw...@googlemail.com

unread,
Jan 24, 2011, 6:10:38 AM1/24/11
to argparse-users
On 23 Jan., 11:23, Steven Bethard <steven.beth...@gmail.com> wrote:

> On Sun, Jan 23, 2011 at 11:21 AM, Steven Bethard
>
> > [...]
> > def flag(value):
> >    value = value.lower()
> >    if value in ("1", "true", "yes", "on"):
> >        return True
> >    elif value in ("0", "false", "no", "off"):
> >        return False
> >    raise ValueError("unknown flag value")
>
> > parser = argparse.ArgumentParser()
> > parser.add_argument("-v", "--verbose", dest="verbose",
> >                    help="Be verbose? (default: %(default)s)",
> >                    nargs='?', type=flag,
> >                    const="no", default="no")
>
> > The nargs='?' says that the argument to '-v' is optional, the
> > default='no' says what to use when no '-v' is present, and the
> > const='no' says what to use when '-v' is present but there's no
> > argument.
>
> Hmm... On second thought I think maybe you actually want const='yes'
> here. But you can play around with it a bit and see.

OK, I tried a variant of this approach:

import argparse

class FlagAction(argparse.Action):
true_choices = ("1", "true", "yes", "on")
false_choices = ("0", "false", "no", "off")

def __init__(self, option_strings, dest, default=False,
help=None):
super(FlagAction, self).__init__(
option_strings=option_strings,
dest=dest,
default="yes" if default else "no",
help=help,
metavar="yes|no",
const="no" if default else "yes",
type=self.str2bool,
nargs="?"
)

def str2bool(self, value):
value = value.lower()
if value in self.true_choices:
return True
elif value in self.false_choices:
return False
else:
raise TypeError("argument {}: invalid flag value: {!r}
(use any of {})".format(
"/".join(self.option_strings),
value,
", ".join(self.true_choices +
self.false_choices)
))

def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)

This doesn't work, but for a completely unrelated reason. It gives me
a recursion error:

Traceback (most recent call last):
File "templ.py", line 37, in <module>
parser.parse_args()
File "/Users/walter/.local/lib/python2.7/argparse.py", line 1678, in
parse_args
args, argv = self.parse_known_args(args, namespace)
File "/Users/walter/.local/lib/python2.7/argparse.py", line 1710, in
parse_known_args
namespace, args = self._parse_known_args(args, namespace)
File "/Users/walter/.local/lib/python2.7/argparse.py", line 1916, in
_parse_known_args
start_index = consume_optional(start_index)
File "/Users/walter/.local/lib/python2.7/argparse.py", line 1856, in
consume_optional
take_action(action, args, option_string)
File "/Users/walter/.local/lib/python2.7/argparse.py", line 1768, in
take_action
argument_values = self._get_values(action, argument_strings)
File "/Users/walter/.local/lib/python2.7/argparse.py", line 2194, in
_get_values
value = self._get_value(action, arg_string)
File "/Users/walter/.local/lib/python2.7/argparse.py", line 2233, in
_get_value
name = getattr(action.type, '__name__', repr(action.type))
File "/Users/walter/.local/lib/python2.7/argparse.py", line 126, in
__repr__
arg_strings.append('%s=%r' % (name, value))
[...]
File "/Users/walter/.local/lib/python2.7/argparse.py", line 126, in
__repr__
arg_strings.append('%s=%r' % (name, value))
RuntimeError: maximum recursion depth exceeded

I guess the problem is that the repr() tries to output the bound
method with outputs the object ...

Implemeting a __repr__ on the action (or using a global function for
type) should solve the problem.

Anyway, thanks for your help.

Servus,
Walter

Steven Bethard

unread,
Jan 24, 2011, 7:26:07 AM1/24/11
to argpars...@googlegroups.com
>  [...]
>  File "/Users/walter/.local/lib/python2.7/argparse.py", line 126, in
> __repr__
>    arg_strings.append('%s=%r' % (name, value))
> RuntimeError: maximum recursion depth exceeded
>
> I guess the problem is that the repr() tries to output the bound
> method with outputs the object ...
>
> Implemeting a __repr__ on the action (or using a global function for
> type) should solve the problem.

The problem stems from:

repr(action.type)

Your type is a bound method, so Python tries to generate its repr as
something like "<bound method FlagAction.str2bool of ...>" and then
tries to generate the repr of the FlagAction object for the "..."
part, which then tries to generate the repr of "action.type", etc.

The solution here is just to make str2bool a function instead of a
method of FlagAction. Note that you probably don't need to raise such
a complicated TypeError if you also specify choices=self.true_choices
+ self.false_choices. Then argparse should generate decent errors on
its own I think.

doerw...@googlemail.com

unread,
Jan 25, 2011, 6:01:14 AM1/25/11
to argparse-users
On 24 Jan., 13:26, Steven Bethard <steven.beth...@gmail.com> wrote:

> On Mon, Jan 24, 2011 at 12:10 PM, doerwal...@googlemail.com
I've now worked around the problem by implementing _get_kwargs():

def _get_kwargs(self):
return [(key, getattr(self, key)) for key in ("option_strings",
"dest", "default", "help")]

> Note that you probably don't need to raise such
> a complicated TypeError if you also specify choices=self.true_choices
> + self.false_choices. Then argparse should generate decent errors on
> its own I think.

That didn't work. I got a generic error:
error: argument -v/--verbose: invalid str2bool value: 'x'

Servus,
Walter

Steven Bethard

unread,
Jan 25, 2011, 8:34:21 AM1/25/11
to argpars...@googlegroups.com
On Tue, Jan 25, 2011 at 12:01 PM, doerw...@googlemail.com
<doerw...@googlemail.com> wrote:
> On 24 Jan., 13:26, Steven Bethard <steven.beth...@gmail.com> wrote:
>> The problem stems from:
>>
>>     repr(action.type)
>>
>> Your type is a bound method, so Python tries to generate its repr as
>> something like "<bound method FlagAction.str2bool of ...>" and then
>> tries to generate the repr of the FlagAction object for the "..."
>> part, which then tries to generate the repr of "action.type", etc.
>>
>> The solution here is just to make str2bool a function instead of a
>> method of FlagAction.
>
> I've now worked around the problem by implementing _get_kwargs():
>
>   def _get_kwargs(self):
>      return [(key, getattr(self, key)) for key in ("option_strings",
> "dest", "default", "help")]

Cool. Yeah, _get_kwargs() is undocumented but that should solve your
problem too.

>> Note that you probably don't need to raise such
>> a complicated TypeError if you also specify choices=self.true_choices
>> + self.false_choices. Then argparse should generate decent errors on
>> its own I think.
>
> That didn't work. I got a generic error:
> error: argument -v/--verbose: invalid str2bool value: 'x'

You can s/str2bool/flag by changing the name of your function. I
assume the remaining problem with this error message is that it
doesn't say something like "use one of ..."? That seems like a good
feature to add - feel free to file a feature request at
bugs.python.org. (Of course new features can only go into Python 3, so
for Python 2, you'll be stuck with your current workarounds.)

doerw...@googlemail.com

unread,
Jan 26, 2011, 6:11:36 AM1/26/11
to argparse-users
On 25 Jan., 14:34, Steven Bethard <steven.beth...@gmail.com> wrote:
ArgumentParser._check_value() seems to be responsible for checking
against choices and raising an exception. However a comment in the
code:

# converted value must be one of the choices (if specified)

indicates that the check is done against the converted value (I guess
that means *after* type is applied).

Servus,
Walter

Steven Bethard

unread,
Jan 26, 2011, 6:48:38 AM1/26/11
to argpars...@googlegroups.com

Good point! I guess it's been way too long since I used choices= for
anything. ;-)

Well, one simplification you could still make is to raise an
ArgumentTypeError instead of just a TypeError. That will then fill in
your "argument {}: " automatically. So you'd use it something like:

raise argparse.ArgumentTypeError(


"invalid flag value: {!r} (use any of {})".format(

value,
", ".join(self.true_choices + self.false_choices)))

Anyway, if you can think of any docs and/or code changes that would
have made this easier for you, please do file a bug report (for docs)
or feature request (for APIs) at bugs.python.org.

doerw...@googlemail.com

unread,
Jan 28, 2011, 11:07:17 AM1/28/11
to argparse-users
On 26 Jan., 12:48, Steven Bethard <steven.beth...@gmail.com> wrote:
> On Wed, Jan 26, 2011 at 12:11 PM, doerwal...@googlemail.com
> <doerwal...@googlemail.com> wrote:
> > On 25 Jan., 14:34, Steven Bethard <steven.beth...@gmail.com> wrote:
> >> You can s/str2bool/flag by changing the name of your function. I
> >> assume the remaining problem with this error message is that it
> >> doesn't say something like "use one of ..."? That seems like a good
> >> feature to add - feel free to file a feature request at
> >> bugs.python.org. (Of course new features can only go into Python 3, so
> >> for Python 2, you'll be stuck with your current workarounds.)
>
> > ArgumentParser._check_value() seems to be responsible for checking
> > against choices and raising an exception. However a comment in the
> > code:
>
> >   # converted value must be one of the choices (if specified)
>
> > indicates that the check is done against the converted value (I guess
> > that means *after* type is applied).
>
> Good point! I guess it's been way too long since I used choices= for
> anything. ;-)
>
> Well, one simplification you could still make is to raise an
> ArgumentTypeError instead of just a TypeError. That will then fill in
> your "argument {}: " automatically. So you'd use it something like:
>
> raise argparse.ArgumentTypeError(
>     "invalid flag value: {!r} (use any of {})".format(
>         value,
>         ", ".join(self.true_choices + self.false_choices)))

OK, I'm doing that now.

> Anyway, if you can think of any docs and/or code changes that would
> have made this easier for you, please do file a bug report (for docs)
> or feature request (for APIs) at bugs.python.org.

Thanks again for your help.

Servus,
Walter
Reply all
Reply to author
Forward
0 new messages