2025, Oct 22 19:00

Fixing argparse mutual exclusivity with nargs='?': Python 3.13.0 bug and the 3.13.1 update

Learn how argparse's mutually exclusive flags with nargs='?' misbehaved in Python 3.13.0, why bare flags bypassed exclusivity, and how 3.13.1 fixes it.

Mutually exclusive flags in argparse are supposed to prevent users from combining conflicting options. There’s a sharp edge, though, when those flags use nargs='?' (an optional value). In Python 3.13.0, two such flags could slip past exclusivity checks if both were given without values. This was surprising because the same flags with values would properly trigger an error.

Minimal setup: optional flag with an optional value

To understand the behavior, start with a single flag using nargs='?'. The flag accepts an optional value, so a bare flag is permitted and results in a special value.

from argparse import ArgumentParser
cli = ArgumentParser()
cli.add_argument("--flag", nargs="?")
opts = cli.parse_args()

With this shape, --flag 123 stores 123, a bare --flag stores None, and omitting --flag also stores None.

Reproducing the exclusivity surprise

Now place two such flags in a mutually exclusive group. Supplying values to both should raise an error, which it does. The twist arises when both flags are provided without values.

from argparse import ArgumentParser
cli = ArgumentParser()
exclusive = cli.add_mutually_exclusive_group()
exclusive.add_argument("--alpha", nargs="?")
exclusive.add_argument("--beta", nargs="?")
parsed = cli.parse_args()

As expected, --alpha 123 --beta 456 fails with “not allowed” because both are present. On Python 3.13.0, however, running --alpha --beta could complete without errors.

What’s actually going on

nargs='?' produces three states: omitted flag, bare flag, and flag with a value. When the default is None, both “omitted” and “bare” collapse to None, which makes it harder to distinguish whether the user provided the bare flag or not. In Python 3.13.0, this interacted poorly with mutual exclusivity: arguments whose value matched the default could be treated as if they weren’t provided at all.

Arguments with the value identical to the default value (e.g. booleans, small integers, empty or 1-character strings) are no longer considered "not present".

This long-known issue was addressed in Python 3.13.1. Reports confirm that current 3.13.x releases (for example, 3.13.5) raise the expected error for --alpha --beta.

Resolution

The fix requires no code changes. Upgrade Python to 3.13.1 or newer. With that in place, the same program enforces mutual exclusivity for both value-supplied and bare flags.

from argparse import ArgumentParser
cli = ArgumentParser()
exclusive = cli.add_mutually_exclusive_group()
exclusive.add_argument("--alpha", nargs="?")
exclusive.add_argument("--beta", nargs="?")
parsed = cli.parse_args()

On fixed versions, invoking --alpha --beta correctly raises “argument --beta: not allowed with argument --alpha”.

Why this matters

Command-line interfaces rely on predictable enforcement of constraints. If mutually exclusive options can coexist silently, downstream logic may receive ambiguous configuration and behave unpredictably. Ensuring consistent parser behavior across environments protects tooling, CI pipelines, and production workflows from subtle misconfigurations.

Practical notes on nargs='?'

Using nargs='?' is most effective together with explicit default and const values, enabling clear three-way semantics. When the default remains None, it becomes harder to determine whether the user supplied a bare flag or omitted it entirely, which can complicate downstream decisions.

Conclusion

If you use mutually exclusive groups with nargs='?', make sure your runtime is Python 3.13.1 or newer to get the corrected behavior. For clearer intent, define default and const when designing optional-value flags, so that “omitted” and “bare” aren’t indistinguishable. Keeping these details in mind will save time debugging ambiguous command lines and help your CLI behave the way users expect.

The article is based on a question from StackOverflow by Jeyekomon and an answer by Jeyekomon.