Title: argparse mutually exclusive group does not exclude in some cases
Type: behavior Stage:
Components: Library (Lib) Versions: Python 3.10
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: kkarbowiak, paul.j3, rhettinger
Priority: normal Keywords:

Created on 2020-07-21 11:46 by kkarbowiak, last changed 2020-07-21 17:08 by paul.j3.

Messages (2)
msg374065 - (view) Author: Krzysiek (kkarbowiak) Date: 2020-07-21 11:46
The documentation for `ArgumentParser.add_mutually_exclusive_group` states: "argparse will make sure that only one of the arguments in the mutually exclusive group was present on the command line".

This is not the case in certain circumstances:

import argparse

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument('-b', nargs='?')

parser.parse_args('-a a -b'.split())

The above code does not produce any error, even though both exclusive arguments are present.

My guess is that the check for mutual exclusion is not done during processing of each command line argument, but rather afterwards. It seems the check only ensures at most one argument from group is not `None`.

The issue exists at least on Python 2.7.13, 3.6, 3.7.5, 3.8, and 3.10.
msg374073 - (view) Author: paul j3 (paul.j3) * (Python triager) Date: 2020-07-21 17:08
This is the result of how default values are handled with '?' (and '*') nargs.

At the start of nested `take_action` function (which handles all actions) is this code:

            argument_values = self._get_values(action, argument_strings)

            # error if this argument is not allowed with other previously
            # seen arguments, assuming that actions that use the default
            # value don't really count as "present"
            if argument_values is not action.default:
                for conflict_action in action_conflicts.get(action, []):
                    if conflict_action in seen_non_default_actions:
                        msg = _('not allowed with argument %s')
                        action_name = _get_action_name(conflict_action)
                        raise ArgumentError(action, msg % action_name)

'get_values' gets the values for this action.  A bare '-b' will be given its 'default'. 

An optional positional is always 'seen', since an empty list satisfies its 'nargs'.  'get_values' assigns the default instead.  This code in take_action allows us to include such positionals in a mutually exclusive group.

This handing of the '?' optional is a byproduct of that code.  Normally we provide both a 'default' and a 'const' with such an argument, giving us a 3-way switch (default, const or user-value).  If either 'default' or 'const' is provided, your '-b' will behave as expected.

I have a feeling that trying to document this edge case will just make things more confusing.
Date User Action Args
2020-07-21 17:08:13paul.j3setmessages: + msg374073
2020-07-21 12:21:54xtreaksetnosy: + rhettinger, paul.j3
2020-07-21 11:46:47kkarbowiakcreate