An argparse argument can be limited to specific values with the choices parameter:
...
parser.add_argument('--val',
choices=['a', 'b', 'c'],
help='Special testing value')
args = parser.parse_args(sys.argv[1:])
See the docs for more details.
Answer from Moshe on Stack Overflowargparse choices allow multiple values
python - How can I pass a list as a command-line argument with argparse? - Stack Overflow
Python: Argument Parsing Validation Best Practices - Stack Overflow
Python argparse select a list from choices - Stack Overflow
Hello
I would like to use the choices options to limit the valid values that are passed as argument to a script
parser = argparse.ArgumentParser()
parser.add_argument('-d', type=int, default=14, help = "enter (%(type)s) number of days to query the cms (default: %(default)s) ")
parser.add_argument('-o', default="Linux", choices=['Linux', 'Windows'], help="pass OS list separated by comma (default: %(default)s)")
parser.add_argument('-s', default="02", choices=['01','02', '03', '04'], help="pass super_status (default: %(default)s)")
args = parser.parse_args()this works fine but I am only allowed to pass one value
eg
script.py -o Windows, Linux -s 01,02
this will fail because it only accept one of the allowed values and not several.
is there a way to use the choices but allow several values i would prefer to do this instead of having to make if/then/else in the script to discard possible invalid arguments.
SHORT ANSWER
Use the nargs option or the 'append' setting of the action option (depending on how you want the user interface to behave).
nargs
parser.add_argument('-l','--list', nargs='+', help='<Required> Set flag', required=True)
# Use like:
# python arg.py -l 1234 2345 3456 4567
nargs='+' takes 1 or more arguments, nargs='*' takes zero or more.
append
parser.add_argument('-l','--list', action='append', help='<Required> Set flag', required=True)
# Use like:
# python arg.py -l 1234 -l 2345 -l 3456 -l 4567
With append you provide the option multiple times to build up the list.
Don't use type=list!!! - There is probably no situation where you would want to use type=list with argparse. Ever.
LONG ANSWER
Let's take a look in more detail at some of the different ways one might try to do this, and the end result.
import argparse
parser = argparse.ArgumentParser()
# By default it will fail with multiple arguments.
parser.add_argument('--default')
# Telling the type to be a list will also fail for multiple arguments,
# but give incorrect results for a single argument.
parser.add_argument('--list-type', type=list)
# This will allow you to provide multiple arguments, but you will get
# a list of lists which is not desired.
parser.add_argument('--list-type-nargs', type=list, nargs='+')
# This is the correct way to handle accepting multiple arguments.
# '+' == 1 or more.
# '*' == 0 or more.
# '?' == 0 or 1.
# An int is an explicit number of arguments to accept.
parser.add_argument('--nargs', nargs='+')
# To make the input integers
parser.add_argument('--nargs-int-type', nargs='+', type=int)
# An alternate way to accept multiple inputs, but you must
# provide the flag once per input. Of course, you can use
# type=int here if you want.
parser.add_argument('--append-action', action='append')
# To show the results of the given option to screen.
for _, value in parser.parse_args()._get_kwargs():
if value is not None:
print(value)
Here is the output you can expect:
$ python arg.py --default 1234 2345 3456 4567
...
arg.py: error: unrecognized arguments: 2345 3456 4567
$ python arg.py --list-type 1234 2345 3456 4567
...
arg.py: error: unrecognized arguments: 2345 3456 4567
$ # Quotes won't help here...
$ python arg.py --list-type "1234 2345 3456 4567"
['1', '2', '3', '4', ' ', '2', '3', '4', '5', ' ', '3', '4', '5', '6', ' ', '4', '5', '6', '7']
$ python arg.py --list-type-nargs 1234 2345 3456 4567
[['1', '2', '3', '4'], ['2', '3', '4', '5'], ['3', '4', '5', '6'], ['4', '5', '6', '7']]
$ python arg.py --nargs 1234 2345 3456 4567
['1234', '2345', '3456', '4567']
$ python arg.py --nargs-int-type 1234 2345 3456 4567
[1234, 2345, 3456, 4567]
$ # Negative numbers are handled perfectly fine out of the box.
$ python arg.py --nargs-int-type -1234 2345 -3456 4567
[-1234, 2345, -3456, 4567]
$ python arg.py --append-action 1234 --append-action 2345 --append-action 3456 --append-action 4567
['1234', '2345', '3456', '4567']
Takeaways:
- Use
nargsoraction='append'nargscan be more straightforward from a user perspective, but it can be unintuitive if there are positional arguments becauseargparsecan't tell what should be a positional argument and what belongs to thenargs; if you have positional arguments thenaction='append'may end up being a better choice.- The above is only true if
nargsis given'*','+', or'?'. If you provide an integer number (such as4) then there will be no problem mixing options withnargsand positional arguments becauseargparsewill know exactly how many values to expect for the option.
- Don't use quotes on the command line1
- Don't use
type=list, as it will return a list of lists- This happens because under the hood
argparseuses the value oftypeto coerce each individual given argument you your chosentype, not the aggregate of all arguments. - You can use
type=int(or whatever) to get a list of ints (or whatever)
- This happens because under the hood
1: I don't mean in general.. I mean using quotes to pass a list to argparse is not what you want.
I prefer passing a delimited string which I parse later in the script. The reasons for this are; the list can be of any type int or str, and sometimes using nargs I run into problems if there are multiple optional arguments and positional arguments.
parser = ArgumentParser()
parser.add_argument('-l', '--list', help='delimited list input', type=str)
args = parser.parse_args()
my_list = [int(item) for item in args.list.split(',')]
Then,
python test.py -l "265340,268738,270774,270817" [other arguments]
or,
python test.py -l 265340,268738,270774,270817 [other arguments]
will work fine. The delimiter can be a space, too, which would though enforce quotes around the argument value like in the example in the question.
Or you can use a lambda type as suggested in the comments by Chepner:
parser.add_argument('-l', '--list', help='delimited list input',
type=lambda s: [int(item) for item in s.split(',')])
The argparse.FileType is a type factory class that can open a file, and of course, in the process raise an error if the file does not exist or cannot be created. You could look at its code to see how to create your own class (or function) to test your inputs.
The argument type parameter is a callable (function, etc) that takes a string, tests it as needed, and converts it (as needed) into the kind of value you want to save to the args namespace. So it can do any kind of testing you want. If the type raises an error, then the parser creates an error message (and usage) and exits.
Now whether that's the right place to do the testing or not depends on your situation. Sometimes opening a file with FileType is fine, but then you have to close it yourself, or wait for the program to end. You can't use that open file in a with open(filename) as f: context. The same could apply to your database. In a complex program you may not want to open or create the file right away.
I wrote for a Python bug/issue a variation on FileType that created a context, an object that could be used in the with context. I also used os tests to check if the file existed or could be created, without actually doing so. But it required further tricks if the file was stdin/out that you don't want to close. Sometimes trying to do things like this in argparse is just more work than it's worth.
Anyways, if you have an easy testing method, you could wrap it in a simple type function like this:
def database(astring):
from os.path import exists
if not database_exists(astring):
raise ValueError # or TypeError, or `argparse.ArgumentTypeError
return astring
parser.add_argument('--database', dest='database',
type = database,
default=None, required=False, help='Database to restore')
I don't think it matters a whole lot whether you implement testing like this in the type or Action. I think the type is simpler and more in line with the developer's intentions.
Surely! You just have to specify a custom action as a class, and override __call__(..). Link to documentation.
Something like:
import argparse
class FooAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if values != "bar":
print("Got value:", values)
raise ValueError("Not a bar!")
setattr(namespace, self.dest, values)
parser = argparse.ArgumentParser()
parser.add_argument("--foo", action=FooAction)
parsed_args = parser.parse_args()
In your particular case, I imagine you'd have DatabaseAction and FileAction (or something like that).
I found a way to get the behavior you want, but with a different syntax than what you present. You have to specify each choice with a unique parameter name/value pair. If that's ok, then the following works:
parser = argparse.ArgumentParser(prog='game.py')
parser.add_argument('--p1', choices=['a', 'b', 'c'], action='append')
args = parser.parse_args(['--p1', 'a', '--p1', 'b'])
print(args)
Result:
Namespace(p1=['a', 'b'])
but this fails appropriately:
parser = argparse.ArgumentParser(prog='game.py')
parser.add_argument('--p1', choices=['a', 'b', 'c'], action='append')
args = parser.parse_args(['--p1', 'a', '--p1', 'b', '--p1', 'x'])
print(args)
Result:
usage: game.py [-h] [--p1 {a,b,c}]
game.py: error: argument --p1: invalid choice: 'x' (choose from 'a', 'b', 'c')
I can find nothing in the docs that suggests that ArgumentParser will take a list of valid values as a parameter value, which is what your version would require.
Using nargs with choices:
In [1]: import argparse
In [2]: p = argparse.ArgumentParser()
In [3]: p.add_argument('-a',nargs='+', choices=['x','y','z'])
Out[3]: _StoreAction(option_strings=['-a'], dest='a', nargs='+', const=None, default=None, type=None, choices=['x', 'y', 'z'], help=None, metavar=None)
In [4]: p.parse_args('-a x y z x x'.split())
Out[4]: Namespace(a=['x', 'y', 'z', 'x', 'x'])
In [5]: p.parse_args('-a x y z w x'.split())
usage: ipython3 [-h] [-a {x,y,z} [{x,y,z} ...]]
ipython3: error: argument -a: invalid choice: 'w' (choose from 'x', 'y', 'z')