Why does argparse include a default for the optional argument, even if the argument is provided?

I have been using argparse since Python 3.6. I am using optional arguments to collect my program parameters. For some of them, I have reasonable defaults, so I set up the parser with a default for this argument.

In [2]: import argparse
   ...: import shlex
   ...: 
   ...: parser = argparse.ArgumentParser()
   ...: parser.add_argument('-s', '--samples', action='store', nargs='+', type=int, required=True,
   ...:                     help='number of samples')
   ...: parser.add_argument('-r', '--regions', action='append', nargs='+', type=str, default=['all'],
   ...:                     help='one or more region names. default to [\'all\']')

      

When the -r / - regions argument is not specified, I expect to see the default configured (and I do).

In [3]: s = '-s 37'
   ...: parser.parse_args(shlex.split(s))
Out[3]: Namespace(regions=['all'], samples=[37])

      

When specifying the -r / - regions argument, I expect to see only the values ​​I provide with the argument, but by default it is also displayed.

In [5]: s = '-s 37 -r foo'
...: parser.parse_args(shlex.split(s))
Out[5]: Namespace(regions=['all', ['foo']], samples=[37])

      

This is not what I expected. I expect the default to be present only when the optional argument is missing. I went through the argparse code. I can't find where the default is enabled. Based on comments , it seems like the logic is to add a default value to the resulting namespace before the actual argument value. I would expect it to be the other way around (i.e., apply the default only when you get to the end of the arguments and don't see the argument that has the default.

Can anyone shed some light on this? Am I misusing or understanding the purpose of the default option for an optional argument? Is there a way to achieve the behavior I'm looking for (for example, if no option is specified, use the default in the namespace)?

+3


source to share


2 answers


The logic for handling default values ​​is to insert all default values ​​at namespace

the beginning of parsing. Then let the parsing replace them. Then there's some tricky logic at the end of the parsing:

for each value in the namespace
   if it is a string and equals the default
      evaluate the string (with `type`) and put it back

      

For normal store

actions, this works great and allows you to specify default values ​​as strings or any value of your choice.

Since append

this will lead to an unexpected value. It puts ['all']

into a namespace and then adds new values ​​to it. Since yours nargs

is '+', it adds a list, resulting in a mix of strings and lists.

The action append

cannot determine if it is adding a new value to the list supplied default

or a list that is the result of several previous ones appends

. The None

default creates an empty list and adds to it.



While this does not work as you expected, it actually gives you a lot of control.

The easiest way is to leave the default None

. After parsing, just check this attribute is None

, and if so, replace it with ['all']

. This is not evil or contrary to the intentions of the developer argparse

developers. Something is easier after all the data has been analyzed.

The issue is related to Python bug / issues, http://bugs.python.org/issue16399 and probably here on SO before. But I suspect the best patch can do is add a documentation note similar to this from optparse

:

"The append action calls the append method on the parameter's current value. This means that any default value must have an append method. This also means that if the default is not empty, the default items will be present in the parsed value for the parameter, with any command line values ​​appended after these defaults ".

See the bug / issue for writing your own Action Action subclass.

+1


source


You are correct that with an action append

and a non-empty default, any provided values ​​are added to the default rather than overridden - this is the expected behavior append

.

For the add action, more appropriate code looks like this

parser.add_argument('-r', '--regions', action='append', type=str)
parser.parse_args('-r foo -r foo2'.split())
Namespace(regions=['foo', 'foo2'])

      

In the source code, you'll note nargs='+'

that the resulting scope value is a list of lists. The append action already makes the variable a list, so nargs is not required.



To then provide a default value that is overridden by the parser makes the default value outside of the parser namespace, for example

_DEFAULT_REGIONS = ['all']

parser = argparse.ArgumentParser()
parser.add_argument('-r', '--regions', action='append', type=str,
                    help="Defaults to %s" % (_DEFAULT_REGIONS))
parser.parse_args(<..>)

regions = parser.regions \
    if parser.regions is not None else _DEFAULT_REGIONS
function_using_regions(regions)

      

Uses parser.regions

if provided, _DEFAULT_REGIONS

otherwise.

+2


source







All Articles