75

I want to get all the remaining unused arguments at once. How do I do it?

parser.add_argument('-i', action='store', dest='i', default='i.log')
parser.add_argument('-o', action='store', dest='o', default='o.log')
SingleNegationElimination
  • 151,563
  • 33
  • 264
  • 304
ggoha
  • 1,996
  • 3
  • 23
  • 31

4 Answers4

125

Use parse_known_args():

args, unknownargs = parser.parse_known_args()
Elmar Peise
  • 14,014
  • 3
  • 21
  • 40
  • 9
    This is helpful when unknown arguments can have leading dashes. For example you are passing them on to another script. – Core Oct 04 '16 at 19:31
  • 1
    Does not work if you want to allow passing conflicting argument names to a command you are wrapping, for example in `docker-compose-wrapper.py --help` vs `docker-compose-wrapper.py exec --help` – pdd Jan 19 '18 at 13:27
  • 4
    There seem to be two solutions to extract possibly conflicting arguments for a wrapped command: 1) use `argparse.REMAINDER` to collect remaining arguments after the last known/expected, or 2) separate the wrapped command with `--` on the command line. – Elmar Peise Jan 19 '18 at 22:52
  • This is useful when you want to forward args to something else – solstice333 Feb 28 '19 at 00:58
  • 1
    Use `argparse.REMAINDER` when you want arguments to have an [argument break](https://unix.stackexchange.com/questions/147143/when-and-how-was-the-double-dash-introduced-as-an-end-of-options-delimiter) requiring `--`. Use `.parse_known_args` when you want to transparently use multiple ArgumentParsers such as with a shared library that accepts arguments. – ThorSummoner Sep 20 '19 at 23:01
  • How can I get parser.print_help() adding a custom info in reasonable way about remaining unkown opts+args being forwarded to some other handler/program? – kxr Sep 13 '21 at 13:27
90

Use argparse.REMAINDER:

parser.add_argument('rest', nargs=argparse.REMAINDER)

Example:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-i', action='store', dest='i', default='i.log')
parser.add_argument('-o', action='store', dest='o', default='o.log')
parser.add_argument('rest', nargs=argparse.REMAINDER)
parser.parse_args(['hello', 'world'])
>>> Namespace(i='i.log', o='o.log', rest=['hello', 'world'])
smac89
  • 39,374
  • 15
  • 132
  • 179
  • 8
    This is IMO the best solution for cases where you are wrapping another command: `wrapper.py --help` --> Your help text `wrapper.py some args --help` --> Passed on to the wrapped command – pdd Jan 19 '18 at 13:25
  • This still raises an exception with optional (specified) args and a 'remaining' argument starting with '--'. The exception doesn't occur if you separate the extra args with --, but this is the same behaviour as using nargs="*". – drevicko Apr 14 '20 at 01:34
  • This works well, but the link in the answer ([link](https://docs.python.org/3/library/argparse.html#argparse-remainder)) is broken in the sense that there's no dedicated `argparse.REMAINDER` section anymore. In fact, `argparse.REMAINDER` is only mentioned once on the entire page, in an unrelated way. Does this equate to `argparse.REMAINDER` being undocumented and, by now, unofficial? – Alex Povel Feb 27 '21 at 13:23
  • @AlexPovel You're right and it is kinda weird. It was there in [3.8](https://docs.python.org/3.8/library/argparse.html#argparse-remainder), but not in the docs for 3.9. There is also no mention of it being [deprecated](https://docs.python.org/3.9/whatsnew/3.9.html#deprecated) in 3.9. I am running the latest python 3.9.2 and typing `argparse.REMAINDER?` in ipython shows that it is still there. It could just be a bug with the document generation process, or they are making some effort to eventually deprecate it. In any case, nothing points strongly to the feature no longer being part of python. – smac89 Feb 27 '21 at 19:59
  • 1
    `argparse.REMAINDER` is now undocumented and considered legacy but I don't believe it's going to be removed soon. See https://github.com/python/cpython/pull/18661 – user2580621 May 10 '21 at 11:44
  • 1
    Nowadays `argparse.REMAINDER` is only mentioned here https://docs.python.org/3/library/argparse.html#intermixed-parsing – Stavros Feb 10 '22 at 07:23
  • If you are wrapping another command, also consider [this answer](https://stackoverflow.com/questions/25872515/python-argparse-treat-arguments-in-different-ways/25873028#25873028) about `--` to pass all following arguments. You can use `args = parser.parse_args()` with `rest = args.rest[1:] if args.rest[:1] == ["--"] else args.rest` to remove the `--` itself. – nh2 Mar 11 '23 at 12:08
67

I went and coded up the three suggestions given as answers in this thread. The test code appears at the bottom of this answer. Conclusion: The best all-around answer so far is nargs=REMAINDER, but it might really depend on your use case.

Here are the differences I observed:

(1) nargs=REMAINDER wins the user-friendliness test.

$ python test.py --help
Using nargs=*          : usage: test.py [-h] [-i I] [otherthings [otherthings ...]]
Using nargs=REMAINDER  : usage: test.py [-h] [-i I] ...
Using parse_known_args : usage: test.py [-h] [-i I]

(2) nargs=* quietly filters out the first -- argument on the command line, which seems bad. On the other hand, all methods respect -- as a way to say "please don't parse any more of these strings as known args."

$ ./test.py hello -- -- cruel -- -- world
Using nargs=*          : ['hello', '--', 'cruel', '--', '--', 'world']
Using nargs=REMAINDER  : ['hello', '--', '--', 'cruel', '--', '--', 'world']
Using parse_known_args : ['hello', '--', '--', 'cruel', '--', '--', 'world']

$ ./test.py -i foo -- -i bar
Using nargs=*          : ['-i', 'bar']
Using nargs=REMAINDER  : ['--', '-i', 'bar']
Using parse_known_args : ['--', '-i', 'bar']

(3) Any method except parse_known_args dies if it tries to parse a string beginning with - and it turns out not to be valid.

$ python test.py -c hello
Using nargs=*          : "unrecognized arguments: -c" and SystemExit
Using nargs=REMAINDER  : "unrecognized arguments: -c" and SystemExit
Using parse_known_args : ['-c', 'hello']

(4) nargs=REMAINDER completely stops parsing when it hits the first unknown argument. parse_known_args will gobble up arguments that are "known", no matter where they appear on the line (and will die if they look malformed).

$ python test.py hello -c world
Using nargs=*          : "unrecognized arguments: -c world" and SystemExit
Using nargs=REMAINDER  : ['hello', '-c', 'world']
Using parse_known_args : ['hello', '-c', 'world']

$ python test.py hello -i world
Using nargs=*          : ['hello']
Using nargs=REMAINDER  : ['hello', '-i', 'world']
Using parse_known_args : ['hello']

$ python test.py hello -i
Using nargs=*          : "error: argument -i: expected one argument" and SystemExit
Using nargs=REMAINDER  : ['hello', '-i']
Using parse_known_args : "error: argument -i: expected one argument" and SystemExit

Here's my test code.

#!/usr/bin/env python

import argparse
import sys

def using_asterisk(argv):
    parser = argparse.ArgumentParser()
    parser.add_argument('-i', dest='i', default='i.log')
    parser.add_argument('otherthings', nargs='*')
    try:
        options = parser.parse_args(argv)
        return options.otherthings
    except BaseException as e:
        return e

def using_REMAINDER(argv):
    parser = argparse.ArgumentParser()
    parser.add_argument('-i', dest='i', default='i.log')
    parser.add_argument('otherthings', nargs=argparse.REMAINDER)
    try:
        options = parser.parse_args(argv)
        return options.otherthings
    except BaseException as e:
        return e

def using_parse_known_args(argv):
    parser = argparse.ArgumentParser()
    parser.add_argument('-i', dest='i', default='i.log')
    try:
        options, rest = parser.parse_known_args(argv)
        return rest
    except BaseException as e:
        return e

if __name__ == '__main__':
    print 'Using nargs=*          : %r' % using_asterisk(sys.argv[1:])
    print 'Using nargs=REMAINDER  : %r' % using_REMAINDER(sys.argv[1:])
    print 'Using parse_known_args : %r' % using_parse_known_args(sys.argv[1:])
Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
  • Another crucial difference is that `argparse.ZERO_OR_MORE` (`*`) will set the `default` passed to `add_argument`, but `argparse.REMAINDER` will not. It's also worth mentioning that `argparse.REMAINDER` is now [an undocumented legacy feature](https://bugs.python.org/issue43343#msg387812) due to "buggy" behavior. – MemReflect Apr 26 '22 at 04:36
22

Another option is to add a positional argument to your parser. Specify the option without leading dashes, and argparse will look for them when no other option is recognized. This has the added benefit of improving the help text for the command:

>>> parser.add_argument('otherthings', nargs='*')
>>> parser.parse_args(['foo', 'bar', 'baz'])
Namespace(i='i.log', o='o.log', otherthings=['foo', 'bar', 'baz'])

and

>>> print parser.format_help()
usage: ipython-script.py [-h] [-i I] [-o O] [otherthings [otherthings ...]]

positional arguments:
  otherthings

optional arguments:
  -h, --help   show this help message and exit
  -i I
  -o O
SingleNegationElimination
  • 151,563
  • 33
  • 264
  • 304