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
            return super(CondAction, self).__call__(parser, namespace, values, option_string)
        except NotImplementedError:

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. '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)

Sample output is:

1646:~/mypy/argdev/usage_groups$ python3 --arg=1 -a1
usage: [-h] [--argument ARGUMENT] [-a A] [-b B]
                        (if --argument then -a and -b are required) 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')

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

usage: [-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 -b2'
'--arg=3 -a1 -b2'

Ones that are supposed to raise errors are:

'--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 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.