Python Argparse conditionally required arguments

PythonArgparse

Python Problem Overview


I have done as much research as possible but I haven't found the best way to make certain cmdline arguments necessary only under certain conditions, in this case only if other arguments have been given. Here's what I want to do at a very basic level:

p = argparse.ArgumentParser(description='...')
p.add_argument('--argument', required=False)
p.add_argument('-a', required=False) # only required if --argument is given
p.add_argument('-b', required=False) # only required if --argument is given

From what I have seen, other people seem to just add their own check at the end:

if args.argument and (args.a is None or args.b is None):
    # raise argparse error here

Is there a way to do this natively within the argparse package?

Python Solutions


Solution 1 - Python

I've been searching for a simple answer to this kind of question for some time. All you need to do is check if '--argument' is in sys.argv, so basically for your code sample you could just do:

import argparse
import sys

if __name__ == '__main__':
	p = argparse.ArgumentParser(description='...')
	p.add_argument('--argument', required=False)
	p.add_argument('-a', required='--argument' in sys.argv) #only required if --argument is given
	p.add_argument('-b', required='--argument' in sys.argv) #only required if --argument is given
	args = p.parse_args()

This way required receives either True or False depending on whether the user as used --argument. Already tested it, seems to work and guarantees that -a and -b have an independent behavior between each other.

Solution 2 - Python

You can implement a check by providing a custom action for --argument, which will take an additional keyword argument to specify which other action(s) should become required if --argument is used.

import argparse

class CondAction(argparse.Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        x = kwargs.pop('to_be_required', [])
        super(CondAction, self).__init__(option_strings, dest, **kwargs)
        self.make_required = x

    def __call__(self, parser, namespace, values, option_string=None):
        for x in self.make_required:
            x.required = True
        try:
            return super(CondAction, self).__call__(parser, namespace, values, option_string)
        except NotImplementedError:
            pass

p = argparse.ArgumentParser()
x = p.add_argument("--a")
p.add_argument("--argument", action=CondAction, to_be_required=[x])

The exact definition of CondAction will depend on what, exactly, --argument should do. But, for example, if --argument is a regular, take-one-argument-and-save-it type of action, then just inheriting from argparse._StoreAction should be sufficient.

In the example parser, we save a reference to the --a option inside the --argument option, and when --argument is seen on the command line, it sets the required flag on --a to True. Once all the options are processed, argparse verifies that any option marked as required has been set.

Solution 3 - Python

Your post parsing test is fine, especially if testing for defaults with is None suits your needs.

http://bugs.python.org/issue11588 'Add "necessarily inclusive" groups to argparse' looks into implementing tests like this using the groups mechanism (a generalization of mutuall_exclusive_groups).

I've written a set of UsageGroups that implement tests like xor (mutually exclusive), and, or, and not. I thought those where comprehensive, but I haven't been able to express your case in terms of those operations. (looks like I need nand - not and, see below)

This script uses a custom Test class, that essentially implements your post-parsing test. seen_actions is a list of Actions that the parse has seen.

class Test(argparse.UsageGroup):
    def _add_test(self):
        self.usage = '(if --argument then -a and -b are required)'
        def testfn(parser, seen_actions, *vargs, **kwargs):
            "custom error"
            actions = self._group_actions
            if actions[0] in seen_actions:
                if actions[1] not in seen_actions or actions[2] not in seen_actions:
                    msg = '%s - 2nd and 3rd required with 1st'
                    self.raise_error(parser, msg)
            return True
        self.testfn = testfn
        self.dest = 'Test'
p = argparse.ArgumentParser(formatter_class=argparse.UsageGroupHelpFormatter)
g1 = p.add_usage_group(kind=Test)
g1.add_argument('--argument')
g1.add_argument('-a')
g1.add_argument('-b')
print(p.parse_args())

Sample output is:

1646:~/mypy/argdev/usage_groups$ python3 issue25626109.py --arg=1 -a1
usage: issue25626109.py [-h] [--argument ARGUMENT] [-a A] [-b B]
                        (if --argument then -a and -b are required)
issue25626109.py: error: group Test: argument, a, b - 2nd and 3rd required with 1st

usage and error messages still need work. And it doesn't do anything that post-parsing test can't.


Your test raises an error if (argument & (!a or !b)). Conversely, what is allowed is !(argument & (!a or !b)) = !(argument & !(a and b)). By adding a nand test to my UsageGroup classes, I can implement your case as:

p = argparse.ArgumentParser(formatter_class=argparse.UsageGroupHelpFormatter)
g1 = p.add_usage_group(kind='nand', dest='nand1')
arg = g1.add_argument('--arg', metavar='C')
g11 = g1.add_usage_group(kind='nand', dest='nand2')
g11.add_argument('-a')
g11.add_argument('-b')

The usage is (using !() to mark a 'nand' test):

usage: issue25626109.py [-h] !(--arg C & !(-a A & -b B))

I think this is the shortest and clearest way of expressing this problem using general purpose usage groups.


In my tests, inputs that parse successfully are:

''
'-a1'
'-a1 -b2'
'--arg=3 -a1 -b2'

Ones that are supposed to raise errors are:

'--arg=3'
'--arg=3 -a1'
'--arg=3 -b2'

Solution 4 - Python

For arguments I've come up with a quick-n-dirty solution like this. Assumptions: (1) '--help' should display help and not complain about required argument and (2) we're parsing sys.argv

p = argparse.ArgumentParser(...)
p.add_argument('-required', ..., required = '--help' not in sys.argv )

This can easily be modified to match a specific setting. For required positionals (which will become unrequired if e.g. '--help' is given on the command line) I've come up with the following: [positionals do not allow for a required=... keyword arg!]

p.add_argument('pattern', ..., narg = '+' if '--help' not in sys.argv else '*' )

basically this turns the number of required occurrences of 'pattern' on the command line from one-or-more into zero-or-more in case '--help' is specified.

Solution 5 - Python

Until http://bugs.python.org/issue11588 is solved, I'd just use nargs:

p = argparse.ArgumentParser(description='...')
p.add_argument('--arguments', required=False, nargs=2, metavar=('A', 'B'))

This way, if anybody supplies --arguments, it will have 2 values.

Maybe its CLI result is less readable, but code is much smaller. You can fix that with good docs/help.

Solution 6 - Python

This is really the same as @Mira 's answer but I wanted to show it for the case where when an option is given that an extra arg is required:

For instance, if --option foo is given then some args are also required that are not required if --option bar is given:

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--option', required=True,
        help='foo and bar need different args')
    
    if 'foo' in sys.argv:
        parser.add_argument('--foo_opt1', required=True,
           help='--option foo requires "--foo_opt1"')
        parser.add_argument('--foo_opt2', required=True,
           help='--option foo requires "--foo_opt2"')
        ...
    
    if 'bar' in sys.argv:
        parser.add_argument('--bar_opt', required=True,
           help='--option bar requires "--bar_opt"')
        ...

It's not perfect - for instance proggy --option foo --foo_opt1 bar is ambiguous but for what I needed to do its ok.

Solution 7 - Python

Here is a simple and clean solution with these advantages:

  • No ambiguity and loss of functionality caused by oversimplified parsing using the in sys.argv test.
  • No need to implement a special argparse.Action or argparse.UsageGroup class.
  • Simple usage even for multiple and complex deciding arguments.

I noticed just one considerable drawback (which some may find desirable): The help text changes according to the state of the deciding arguments.

The idea is to use argparse twice:

  1. Parse the deciding arguments instead of the oversimplified use of the in sys.argv test. For this we use a short parser not showing help and the method .parse_known_args() which ignores unknown arguments.
  2. Parse everything normally while reusing the parser from the first step as a parent and having the results from the first parser available.
import argparse

# First parse the deciding arguments.
deciding_args_parser = argparse.ArgumentParser(add_help=False)
deciding_args_parser.add_argument(
        '--argument', required=False, action='store_true')
deciding_args, _ = deciding_args_parser.parse_known_args()

# Create the main parser with the knowledge of the deciding arguments.
parser = argparse.ArgumentParser(
        description='...', parents=[deciding_args_parser])
parser.add_argument('-a', required=deciding_args.argument)
parser.add_argument('-b', required=deciding_args.argument)
arguments = parser.parse_args()

print(arguments)

Solution 8 - Python

Add additional simple "pre"parser to check --argument, but use parse_known_args() .

pre = argparse.ArgumentParser()
pre.add_argument('--argument', required=False, action='store_true', default=False)
args_pre=pre.parse_known_args()

p = argparse.ArgumentParser()
p.add_argument('--argument', required=False)
p.add_argument('-a', required=args_pre.argument)
p.add_argument('-b', required=not args_pre.argument)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDJMcCarthy12View Question on Stackoverflow
Solution 1 - PythonMiraView Answer on Stackoverflow
Solution 2 - PythonchepnerView Answer on Stackoverflow
Solution 3 - PythonhpauljView Answer on Stackoverflow
Solution 4 - PythonemveeView Answer on Stackoverflow
Solution 5 - PythonYajoView Answer on Stackoverflow
Solution 6 - PythonkeithpjolleyView Answer on Stackoverflow
Solution 7 - Pythonpabouk - Ukraine stay strongView Answer on Stackoverflow
Solution 8 - Pythonuser18374507View Answer on Stackoverflow