2025, Oct 22 19:16
Взаимоисключающие флаги с nargs='?' в argparse: баг 3.13.0 и фикс в Python 3.13.1
Разбираем баг argparse с nargs='?' во взаимно исключаемых флагах: почему --alpha --beta проходили в Python 3.13.0 и как это исправлено в 3.13.1. Обновляйтесь.
Взаимоисключающие флаги в argparse нужны затем, чтобы пользователи не могли комбинировать конфликтующие опции. Однако есть острый угол, когда флаги используют nargs='?' (необязательное значение). В Python 3.13.0 два таких флага могли пройти мимо проверки эксклюзивности, если оба указывались без значений. Это удивляло, потому что те же флаги с явными значениями корректно вызывали ошибку.
Минимальный пример: опциональный флаг с необязательным значением
Чтобы понять поведение, начнём с одного флага с nargs='?'. Такой флаг принимает необязательное значение, поэтому «голый» флаг допустим и приводит к специальному значению.
from argparse import ArgumentParser
cli = ArgumentParser()
cli.add_argument("--flag", nargs="?")
opts = cli.parse_args()
В такой конфигурации --flag 123 сохраняет 123, одинокий --flag сохраняет None, а отсутствие --flag тоже приводит к None.
Воспроизводим неожиданность с взаимной исключаемостью
Теперь поместим два таких флага в группу взаимно исключаемых. Передача значений для обоих должна вызывать ошибку — так и происходит. Но тонкость проявляется, когда оба флага указаны без значений.
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()
Как и ожидалось, --alpha 123 --beta 456 завершается ошибкой «not allowed», потому что присутствуют оба флага. Однако в Python 3.13.0 запуск --alpha --beta мог завершиться без ошибок.
Что на самом деле происходит
nargs='?' даёт три состояния: флаг опущен, флаг без значения и флаг со значением. Когда значение по умолчанию — None, случаи «опущен» и «голый» схлопываются в None, поэтому сложнее понять, указал ли пользователь флаг без значения или не передавал его вовсе. В Python 3.13.0 это плохо сочеталось с взаимной исключаемостью: аргументы, значение которых совпадало с умолчанием, могли считаться так, словно их не передавали.
Аргументы, значение которых идентично значению по умолчанию (например, булевы значения, маленькие целые числа, пустые строки или строки длиной в 1 символ), больше не считаются «отсутствующими».
Эта давняя проблема была исправлена в Python 3.13.1. Отчёты подтверждают, что текущие релизы 3.13.x (например, 3.13.5) выдают ожидаемую ошибку для --alpha --beta.
Решение
Исправление не требует изменений в коде. Обновите Python до версии 3.13.1 или новее. После этого та же программа обеспечивает взаимную исключаемость и для флагов со значением, и для «голых».
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()
На исправленных версиях вызов --alpha --beta корректно приводит к ошибке “argument --beta: not allowed with argument --alpha”.
Почему это важно
Командные интерфейсы держатся на предсказуемом соблюдении ограничений. Если взаимно исключающие опции тихо сосуществуют, последующая логика получает двусмысленную конфигурацию и может вести себя непредсказуемо. Единое поведение парсера в разных окружениях защищает инструменты, CI‑пайплайны и рабочие процессы в продакшене от тонких ошибок конфигурации.
Практические замечания о nargs='?'
Использовать nargs='?' особенно эффективно вместе с явными значениями default и const — это даёт понятную трёхсоставную семантику. Когда по умолчанию остаётся None, сложнее понять, указал ли пользователь «голый» флаг или вовсе его опустил, что может усложнить последующие решения.
Итог
Если вы используете взаимно исключающие группы с nargs='?', убедитесь, что в рантайме — Python 3.13.1 или новее, чтобы получить исправленное поведение. Для большей ясности задавайте default и const при проектировании флагов с необязательным значением — так «опущенный» и «голый» случаи не будут неотличимы. Эти детали сэкономят время на отладке неоднозначных команд и помогут вашему CLI вести себя так, как ожидают пользователи.