Python Argparse conditionally required arguments

Posted on

Question :

Python Argparse conditionally required arguments

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?

Answer #1:

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.

Answered By: Mira

Answer #2:

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.

Answered By: chepner

Answer #3:

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'
Answered By: hpaulj

Answer #4:

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.

Answered By: haavee

Answer #5:

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.

Answered By: Yajo

Answer #6:

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.

Answered By: keithpjolley

Leave a Reply

Your email address will not be published.