Reputation: 20774
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
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
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
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
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