a06e
a06e

Reputation: 20774

argparse doesn't cast default numeric arguments?

Some lines of code are worth a thousand words. Create a python file test.py with the following:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--G', type=float, default=1, nargs='?')
args = parser.parse_args()
print (args.G / 3)

Then, in a terminal:

python test.py

gives 0, while:

python test.py --G 1

gives 0.333333333. I think this is because argparse doesn't seem to cast default arguments to their proper type (so 1 remains an int in default=1, in spite of the type=float), but it casts the argument if it is given explicitly.

I think this is inconsistent and prone to errors. Is there a reason why argparse behaves this way?

Upvotes: 3

Views: 4968

Answers (4)

hogsmeade89
hogsmeade89

Reputation: 11

If the default value is a string, the parser parses the value as if it were a command-line argument. In particular, the parser applies any type conversion argument, if provided, before setting the attribute on the Namespace return value. Otherwise, the parser uses the value as is:

parser = argparse.ArgumentParser()
parser.add_argument('--length', default='10', type=int)
parser.add_argument('--width', default=10.5, type=int)
args = parser.parse_args()

Here, args.length will be int 10 and args.width will be float 10.5, just as the default value.

The example is from https://docs.python.org/3.8/library/argparse.html#default.

At first, I also have the same question. Then I notice this example, it explains the question pretty clearly.

Upvotes: 1

hpaulj
hpaulj

Reputation: 231395

The other answers are right - only string defaults are passed through the type function. But there seems to be some reluctance to accept that logic.

Maybe this example will help:

import argparse
def mytype(astring):
    return '['+astring+']'
parser = argparse.ArgumentParser()
parser.add_argument('--foo', type=mytype, default=1)
parser.add_argument('--bar', type=mytype, default='bar')
print parser.parse_args([])
print mytype(1)

produces:

0923:~/mypy$ python stack35429336.py 
Namespace(bar='[bar]', foo=1)
Traceback (most recent call last):
  File "stack35429336.py", line 8, in <module>
    print mytype(1)
  File "stack35429336.py", line 3, in mytype
    return '['+astring+']'
TypeError: cannot concatenate 'str' and 'int' objects

I define a type function - it takes a string input, and returns something - anything that I want. And raises an error if it can't return that. Here I just append some characters to the string.

When the default is a string, it gets modified. But when it is a number (not a string) it is inserted without change. In fact as written mytype raises an error if given a number.

The argparse type is often confused with the function type(a). The latter returns values like int,str,bool. Plus the most common examples are int and float. But in argparse float is used as a function

float(x) -> floating point number
    Convert a string or number to a floating point number, if possible.

type=bool is a common error. Parsing boolean values with argparse. bool() does not convert the string 'False' to the boolean False.

In [50]: bool('False')
Out[50]: True

If argparse passed every default through the type function, it would be difficult to place values like None or False in the namespace. No builtin function converts a string to None.

The key point is that the type parameter is a function (callable), not a casting operation or target.

For further clarification - or confusion - explore the default and type with nargs=2 or action='append'.

Upvotes: 2

deceze
deceze

Reputation: 522125

The purpose of type is to sanitise and validate the arbitrary input you receive from the command line. It's a first line of defence against bogus input. There is no need to "defend" yourself against the default value, because that's the value you have decided on yourself and you are in power and have full control over your application. type does not mean that the resulting value after parse_args() must be that type; it means that any input should be that type/should be the result of that function.

What the default value is is completely independent of that and entirely up to you. It's completely conceivable to have a default value of False, and if a user inputs a number for this optional argument, then it'll be a number instead.

Upgrading a comment below to be included here: Python's philosophy includes "everyone is a responsible adult and Python won't overly question your decisions" (paraphrased). This very much applies here I think.

The fact that argparse does cast default strings could be explained by the fact that CLI input is always a string, and it's possible that you're chaining several argparsers, and the default value is set by a previous input. Anything that's not a string type however must have been explicitly chosen by you, so it won't be further mangled.

Upvotes: 1

Lim H.
Lim H.

Reputation: 10050

I think the 1 in default=1 is evaluated when you call parser.add_argument, where as the non-default value you pass as argument is evaluated at runtime, and therefore can be converted to float by argparse. It's not how argparse behaves; it's how python behaves in general. Consider this

def foo(x=1):
    # there is no way to tell the function body that you want 1.0
    # except for explicitly conversion
    # because 1 is passed by value, which has already been evaluated
    # and determined to be an int
    print (x/3)

HUGE EDIT: Ha, I understand your point now and I think it's reasonable. So I dig into the source code and looks what I found:

https://hg.python.org/cpython/file/3.5/Lib/argparse.py#l1984

So argparse DOES appear to make sure your default value is type compliant, so long as it's a string. Try:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--G', type=float, default='1', nargs='?')
args = parser.parse_args()
print (args.G / 3)

Not sure if that's a bug or a compromise...

Upvotes: 2

Related Questions