You mention in passing other optionals, so I assume there's good incentive to keep argparse. Assuming there aren't other positionals, you could try:
In [23]: parser = argparse.ArgumentParser()
In [24]: parser.add_argument('--other'); # other optionals
In [25]: parser.add_argument('--filelist');
In [26]: parser.add_argument('files', nargs='*');
This accepts the '--filelist', without requiring the positional files, (with a resulting empty list):
In [27]: parser.parse_args('--filelist alist'.split())
Out[27]: Namespace(other=None, filelist='alist', files=[])
Other the files - but restricting that list to 2 requires your own testing after parsing:
In [28]: parser.parse_args('file1 file2'.split())
Out[28]: Namespace(other=None, filelist=None, files=['file1', 'file2'])
But it also accepts both forms. Again your own post parsing code will have to sort out the conflicting message:
In [29]: parser.parse_args('file1 file2 --filelist xxx'.split())
Out[29]: Namespace(other=None, filelist='xxx', files=['file1', 'file2'])
And you have to deal with the case where neither is provided:
In [30]: parser.parse_args('--other foobar'.split())
Out[30]: Namespace(other='foobar', filelist=None, files=[])
The help:
In [32]: parser.print_help()
usage: ipykernel_launcher.py [-h] [--other OTHER] [--filelist FILELIST]
[files ...]
positional arguments:
files
optional arguments:
-h, --help show this help message and exit
--other OTHER
--filelist FILELIST
So what I've created accepts both forms of input, but does not constrain them.
I tried using a mutually_exclusive_group, but that only works with nargs='?'. I thought at one time nargs='*' worked in a group, but either my memory is wrong, or there was a patch that changed things. A positional in a m-x-group has to have 'required=False' value.
A subparsers is actually a special kind of positional. So if you created parser with usage 'prog file1 file2 {cmd1, cmd2}' it would still expect the 2 file names before checking on the third string. All positionals, including subparsers are handled by position, not value. A flagged argument (optional) can occur in any order, but does not override the positionals.
And if you define the subparsers first, then the first positional has to be one of those commands. subparsers are not required (by default), but that doesn't change the handling of other positionals.
working mutually exclusive group
nargs='*' does work if we also specify a default. This is a undocumented detail. I forgot about it until I looked at the code (specifically the _get_positional_kwargs method). A * is marked as required unless it's given a default. It's been discussed in one or more bug/issue some time ago.
So can make positional list and '--file_list' mutually exclusive
In [71]: parser = argparse.ArgumentParser()
In [72]: parser.add_argument('--other'); # other optionals
In [73]: group = parser.add_mutually_exclusive_group(required=True)
In [74]: group.add_argument('--file_list');
In [75]: group.add_argument('files', nargs='*', default=[]);
Good cases:
In [76]: parser.parse_args('--other foobar file1 file2'.split())
Out[76]: Namespace(other='foobar', file_list=None, files=['file1', 'file2'])
In [77]: parser.parse_args('--other foobar --file_list file3'.split())
Out[77]: Namespace(other='foobar', file_list='file3', files=[])
If both are used:
In [78]: parser.parse_args('--other foobar --file_list file3 file1 file2'.split())
usage: ipykernel_launcher.py [-h] [--other OTHER] [--file_list FILE_LIST]
[files ...]
ipykernel_launcher.py: error: argument files: not allowed with argument --file_list
And if neither is provided:
In [79]: parser.parse_args('--other foobar'.split())
usage: ipykernel_launcher.py [-h] [--other OTHER] [--file_list FILE_LIST]
[files ...]
ipykernel_launcher.py: error: one of the arguments --file_list files is required
Usage now looks like:
In [81]: parser.print_usage()
usage: ipykernel_launcher.py [-h] [--other OTHER] [--file_list FILE_LIST]
[files ...]
It still doesn't control the number of files - except there must be atleast one.