3

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?

a06e
  • 18,594
  • 33
  • 93
  • 169
  • *You* are deciding on the default value. It's *your* responsibility to be consistent. Why should Python question your decisions? No value was supplied on the command line, you already did supply a default value, why should Python filter it through some function? What if you mean to use `False` as the default value? – deceze Feb 16 '16 at 10:13
  • @deceze I don't say there should be a filtering, but an implicit cast. If I decide to pass `False` as default argument, it should be converted to `0.0`. That's only my opinion, of course. I am open to arguments justifying `argparse` current behavior. – a06e Feb 16 '16 at 10:15
  • Why the downvote? Please leave a comment. – a06e Feb 16 '16 at 10:17
  • That's what I'm arguing: argparse should *not* further modify the default value you have decided on. Because it would restrict what you're able to do. `type` means *filter any input through this function*, but that does not apply when there's no input, because you already have full control over the default value yourself and don't need to further sanitise it. – deceze Feb 16 '16 at 10:18
  • default=1.0 does not work? – YOU Feb 16 '16 at 10:25
  • @deceze I tried with **'1.0'** (string) instead of 1 for default and it did convert it to float, so I think there is some conversion involved. Not sure why 1 is not being converted. – Ankit Jaiswal Feb 16 '16 at 10:25
  • @YOU Yes, of course. The point is that it is not obvious that you should not use `$default=1` (since it would could lead to errors, as my example shows). – a06e Feb 16 '16 at 10:25

4 Answers4

2

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...

Lim H.
  • 9,870
  • 9
  • 48
  • 74
  • That's my point. I think that for consistency, you should do something like `x = float(x)` inside `argparse`. Otherwise, the situation that I described above can lead to hard to catch errors. – a06e Feb 16 '16 at 10:14
  • Where of course, `float` refers to the type explicitly given in the `type=float` specification to `argparse` – a06e Feb 16 '16 at 10:14
  • @becko there you go! I understand you now. Updated! – Lim H. Feb 16 '16 at 10:24
  • The action on that line of `argparse` is very much intended. Only string values are passed through the `type` *function*. In this context `float` is a function that converts a string. – hpaulj Feb 16 '16 at 17:10
2

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'.

Community
  • 1
  • 1
hpaulj
  • 221,503
  • 14
  • 230
  • 353
1

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.

deceze
  • 510,633
  • 85
  • 743
  • 889
  • I beg to differ. Please see my answer. I found in argparse source code that it does try to make sure the default value is type-compliant. – Lim H. Feb 16 '16 at 10:24
  • ...to some extend it does. You can still use `False` as default value if you like (for example)... – deceze Feb 16 '16 at 10:25
  • So sometimes it does and sometimes it doesn't. That seems inconsistent. I think it would be simpler if it always converted to the explicit type. – a06e Feb 16 '16 at 10:27
  • @becko your best bet is to file a bug report and ask the author themselves. Not sure if that's intentional – Lim H. Feb 16 '16 at 10:27
  • IMO this at least partly goes back to Python's philosophy of "everyone is a responsible adult and Python won't overly question your decisions" (paraphrased). @becko The exception with strings *possibly* comes from the fact that CLI input is always a string type, and perhaps you're chaining a bunch of argparsers together and the default value could come from a previous argparse, while anything that's not a string must have been set by you explicitly already. – deceze Feb 16 '16 at 10:28
  • 1
    @deceze that sounds plausible :) – Lim H. Feb 16 '16 at 10:30
1

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.