100

I thought that nargs='*' was enough to handle a variable number of arguments. Apparently it's not, and I don't understand the cause of this error.

The code:

p = argparse.ArgumentParser()
p.add_argument('pos')
p.add_argument('foo')
p.add_argument('--spam', default=24, type=int, dest='spam')
p.add_argument('vars', nargs='*')

p.parse_args('1 2 --spam 8 8 9'.split())

I think the resulting namespace should be Namespace(pos='1', foo='2', spam='8', vars=['8', '9']). Instead, argparse gives this error:

usage: prog.py [-h] [--spam SPAM] pos foo [vars [vars ...]]
error: unrecognized arguments: 9 8

Basically, argparse doesn't know where to put those additional arguments... Why is that?

rubik
  • 8,814
  • 9
  • 58
  • 88

4 Answers4

96

For anyone who doesn't know what is nargs:

nargs stands for Number Of Arguments

  • 3: 3 values, can be any number you want
  • ?: a single value, which can be optional
  • *: a flexible number of values, which will be gathered into a list
  • +: like *, but requiring at least one value
  • argparse.REMAINDER: all the values that are remaining in the command line

Example:

Python

import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('--input', action='store', type=int, nargs=3)

args = my_parser.parse_args()

print(args.input)

Console

$ python nargs_example.py --input 42
usage: nargs_example.py [-h] [--input INPUT INPUT INPUT]
nargs_example.py: error: argument --input: expected 3 arguments

$ python nargs_example.py --input 42 42 42
[42, 42, 42]

See more

HoangYell
  • 4,100
  • 37
  • 31
60

The relevant Python bug is Issue 15112.

argparse: nargs='*' positional argument doesn't accept any items if preceded by an option and another positional

When argparse parses ['1', '2', '--spam', '8', '8', '9'] it first tries to match ['1','2'] with as many of the positional arguments as possible. With your arguments the pattern matching string is AAA*: 1 argument each for pos and foo, and zero arguments for vars (remember * means ZERO_OR_MORE).

['--spam','8'] are handled by your --spam argument. Since vars has already been set to [], there is nothing left to handle ['8','9'].

The programming change to argparse checks for the case where 0 argument strings is satisfying the pattern, but there are still optionals to be parsed. It then defers the handling of that * argument.

You might be able to get around this by first parsing the input with parse_known_args, and then handling the remainder with another call to parse_args.

To have complete freedom in interspersing optionals among positionals, in issue 14191, I propose using parse_known_args with just the optionals, followed by a parse_args that only knows about the positionals. The parse_intermixed_args function that I posted there could be implemented in an ArgumentParser subclass, without modifying the argparse.py code itself.


Here's a way of handling subparsers. I've taken the parse_known_intermixed_args function, simplified it for presentation sake, and then made it the parse_known_args function of a Parser subclass. I had to take an extra step to avoid recursion.

Finally I changed the _parser_class of the subparsers Action, so each subparser uses this alternative parse_known_args. An alternative would be to subclass _SubParsersAction, possibly modifying its __call__.

from argparse import ArgumentParser

def parse_known_intermixed_args(self, args=None, namespace=None):
    # self - argparse parser
    # simplified from http://bugs.python.org/file30204/test_intermixed.py
    parsefn = super(SubParser, self).parse_known_args # avoid recursion

    positionals = self._get_positional_actions()
    for action in positionals:
        # deactivate positionals
        action.save_nargs = action.nargs
        action.nargs = 0

    namespace, remaining_args = parsefn(args, namespace)
    for action in positionals:
        # remove the empty positional values from namespace
        if hasattr(namespace, action.dest):
            delattr(namespace, action.dest)
    for action in positionals:
        action.nargs = action.save_nargs
    # parse positionals
    namespace, extras = parsefn(remaining_args, namespace)
    return namespace, extras

class SubParser(ArgumentParser):
    parse_known_args = parse_known_intermixed_args

parser = ArgumentParser()
parser.add_argument('foo')
sp = parser.add_subparsers(dest='cmd')
sp._parser_class = SubParser # use different parser class for subparsers
spp1 = sp.add_parser('cmd1')
spp1.add_argument('-x')
spp1.add_argument('bar')
spp1.add_argument('vars',nargs='*')

print parser.parse_args('foo cmd1 bar -x one 8 9'.split())
# Namespace(bar='bar', cmd='cmd1', foo='foo', vars=['8', '9'], x='one')
Samuel Harmer
  • 4,264
  • 5
  • 33
  • 67
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • Oh this is good news. So are you saying that if I take the two functions you added in http://bugs.python.org/file30422/intermixed.patch and I implement in my own subclass I'll be able to get around the problem? What about the `_get_values()` function, is the two-lines change to it strictly necessary? I find it difficult to understand how `SUPPRESS` is used. – rubik Nov 23 '13 at 20:51
  • Oh no, bad news just arrived. I read that this is incompatible with subparsers, a feature I absolutely need. Is that temporary or just impossible to realize? Instinctively I'd say that it should be possible to do, because the subparser is specified as second argument, then the parsing happens normally. Am I wrong? – rubik Nov 23 '13 at 20:54
  • I tried 2 different ways of temporarily disabling the `positional` arguments. The first was `nargs=0`. The second was `nargs=SUPPRESS`. I would use the first if implementing my own subclass. `SUPPRESS` has some advantages but requires deeper changes to `argparse. – hpaulj Nov 23 '13 at 21:47
  • http://bugs.python.org/file30204/test_intermixed.py is a better example of patching your own parser. – hpaulj Nov 23 '13 at 22:02
  • My `parse_intermixed_arg` function does object if there is a subparser argument (`argparse.PARSER`). I have added some code that gets around that constraint, by using the `intermixed` approach only in the subparsers. – hpaulj Nov 24 '13 at 04:03
  • Thanks! I'll do as you suggest. – rubik Nov 25 '13 at 14:18
  • 2
    Could do with an update now that issue14191 is fixed – wim Sep 25 '20 at 20:23
16

Simple solution: Specify the --spam flag before specifying pos and foo:

p = argparse.ArgumentParser()
p.add_argument('pos')
p.add_argument('foo')
p.add_argument('--spam', default=24, type=int, dest='spam')
p.add_argument('vars', nargs='*')

p.parse_args('--spam 8 1 2 8 9'.split())

The same works if you place the --spam flag after specifying your variable arguments.

p = argparse.ArgumentParser()
p.add_argument('pos')
p.add_argument('foo')
p.add_argument('--spam', default=24, type=int, dest='spam')
p.add_argument('vars', nargs='*')

p.parse_args('1 2 8 9 --spam 8'.split())

EDIT: For what it's worth, it seems that changing the * to a + will also fix the error.

p = argparse.ArgumentParser()
p.add_argument('pos')
p.add_argument('foo')
p.add_argument('--spam', default=24, type=int, dest='spam')
p.add_argument('vars', nargs='+')

p.parse_args('1 2 --spam 8 8 9'.split())
caleb531
  • 4,111
  • 6
  • 31
  • 41
  • 7
    Well yes I could do that, but that's not what I want. I'm building a command line tool, so I'm not the one who will write the arguments. I cannot force on the user a specific solution. The code I wrote should work and should be flexible. I really don't know why it fails. – rubik Nov 23 '13 at 19:09
  • 2
    True, but it's quite possible that `argparse` cannot handle this particular situation. It's also quite possible that the rules for argument parsing do not allow for the arguments to be positioned in this way. Anyway, I've amended my answer with another potential solution, FWIW. – caleb531 Nov 23 '13 at 19:30
  • You're right, eventually it turned up to be an argparse bug, as the other answer describes. – rubik Nov 23 '13 at 20:43
0

If you expect to have at least one optional argument then p.add_argument('vars', nargs='+') will work in your specific case

Bostone
  • 36,858
  • 39
  • 167
  • 227