12

I want to use argparse's {choices} parameter, but allow the user to input any number of items from choices. For example if choices is [1,2,3], I would like the following to be valid:

--arg 1
--arg 1,2
--arg 1,3

etc.

However it seems like choices doesn't accept a comma-separated input when using nargs="+". Is there any way around this? I still want to enforce that the passed in options are within the set of choices that I defined, to error-check for weird inputs.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
jj172
  • 751
  • 2
  • 9
  • 35
  • Does the list have to be comma-separated? The standard `argparse` format would be space separated: https://docs.python.org/3/library/argparse.html#nargs – Blorgbeard Apr 13 '18 at 20:01
  • @Blorgbeard because of the limitation of argparse which won’t allow having both a flag with nargs and positional arguments. See [the issue itself](https://bugs.python.org/issue9338) or [this SO question](https://stackoverflow.com/q/26985650/2622010) – Vser Oct 20 '22 at 17:41
  • @jj172 see my answer [here](https://stackoverflow.com/a/74144448/2622010). I could not answer your question as it is closed as duplicate. – Vser Oct 20 '22 at 18:08

3 Answers3

15

I agree - You would think that the API would allow someone to do that by now right?

Well anyways, the workaround I've always used was the following:

p = argparse.ArgumentParser(description="Why doesn't argparse support list of args?")
parser.add_argument('--arg', type=str)
arg_list = parser.parse_args().args.split(",")
# if you wanted integers:
arg_list = [int(x) for x in arg_list]

AKA: Take in a string and process it yourself.

OneRaynyDay
  • 3,658
  • 2
  • 23
  • 56
  • Hey sorry, I actually changed my question a bit. I want to enforce using "choices", which I didn't initially mention - any ideas on how to have that work? – jj172 Apr 13 '18 at 20:05
  • Do you need to use choices? Why not just use a set and check the results afterwards? – OneRaynyDay Apr 13 '18 at 20:08
  • I like the CLI that choices provides, offering the user feedback if they enter an incorrect option. Honestly I think I'm leaning towards just not using comma-separated inputs :P – jj172 Apr 13 '18 at 20:08
  • That's fair. I think then you should take @toheedNiaz's answer :) – OneRaynyDay Apr 13 '18 at 20:13
  • It's hard to believe that it doesn't accept commas as delimiters. Yes, you can work around that, but it's ugly. – Ben Slade Mar 18 '22 at 21:28
1

This one-line code gives you all possible subsets of your choices.

    from itertools import combinations, chain
    allsubsets = lambda n: list(chain(*[combinations(range(n), ni) for ni in range(n+1)]))
1
import argparse, sys
print(sys.argv)
parser = argparse.ArgumentParser()
parser.add_argument('--arg', nargs='+', choices=[1,2,3], type=int)
args = parser.parse_args()
print(args)

some runs

1455:~/mypy$ python stack49824248.py --arg 1 
['stack49824248.py', '--arg', '1']
Namespace(arg=[1])

1455:~/mypy$ python stack49824248.py --arg 1 3 2 1
['stack49824248.py', '--arg', '1', '3', '2', '1']
Namespace(arg=[1, 3, 2, 1])

1456:~/mypy$ python stack49824248.py --arg 1,2
['stack49824248.py', '--arg', '1,2']
usage: stack49824248.py [-h] [--arg {1,2,3} [{1,2,3} ...]]
stack49824248.py: error: argument --arg: invalid int value: '1,2'

The shell, together with the interpreter, splits the input on spaces, and provides a list of strings in sys.argv. That's what parser handles.

With +, the --arg action accepts a list of strings (to the end or next flag). Each string is passed through the type function, and the result compared to the choices (if provided). In this case, type is int, so the choices can be integers as well. Without the type, choices would have to be ['1','2','3'].

If I change the argument to:

parser.add_argument('--arg', nargs='+', choices=['1','2','3','1,2','2,3'])

it will accept some strings with commas:

1456:~/mypy$ python stack49824248.py --arg 1
['stack49824248.py', '--arg', '1']
Namespace(arg=['1'])
1505:~/mypy$ python stack49824248.py --arg 1,2
['stack49824248.py', '--arg', '1,2']
Namespace(arg=['1,2'])
1505:~/mypy$ python stack49824248.py --arg 1,2,3
['stack49824248.py', '--arg', '1,2,3']
usage: stack49824248.py [-h] [--arg {1,2,3,1,2,2,3} [{1,2,3,1,2,2,3} ...]]
stack49824248.py: error: argument --arg: invalid choice: '1,2,3' (choose from '1', '2', '3', '1,2', '2,3')

I didn't include the '1,2,3' choice, so it rejected that. Note also that I dropped the int type, since int('1,2') will fail.

So if you need to accept '1,2,3', do your own split and choices test after parsing (or possibly as a custom Action class).

In [16]: [(int(x) in [1,2,3]) for x in '1,2,3'.split(',')]
Out[16]: [True, True, True]
In [17]: [(int(x) in [1,2,3]) for x in '1,2,4'.split(',')]
Out[17]: [True, True, False]
In [18]: [(int(x) in [1,2,3]) for x in '1,a,4'.split(',')]
....
ValueError: invalid literal for int() with base 10: 'a'
hpaulj
  • 221,503
  • 14
  • 230
  • 353