Python を 3.14 に上げたら、コマンドライン引数の定義で落ちます。
ValueError: argument groups cannot be nested
argparse で、引数グループから別の引数グループを作っているコードです。
import argparse
parser = argparse.ArgumentParser()
database = parser.add_argument_group("database")
credentials = database.add_argument_group("credentials") # ここで ValueError
credentials.add_argument("--user")
parser.parse_args() を呼ぶ前、グループを定義した時点で落ちます。
直し方
両方のグループを ArgumentParser から作ります。
parser = argparse.ArgumentParser()
database = parser.add_argument_group("database")
credentials = parser.add_argument_group("credentials") # parser から作る
credentials.add_argument("--user")
この平坦化で失うものはありません。 理由は次の節のとおりで、入れ子は 3.13 の時点で見た目にも反映されていなかったからです。
入れ子は 3.13 でも動いていませんでした
「3.14 で禁止された」と聞くと、3.13 までは入れ子が機能していたように思えます。していません。
Python 3.13 で、入れ子にしたグループへ引数を足して --help を表示させた結果です(実測)。
parser = argparse.ArgumentParser(prog="app")
database = parser.add_argument_group("database")
database.add_argument("--host")
credentials = database.add_argument_group("credentials") # 3.13 では通る
credentials.add_argument("--user")
parser.print_help()
usage: app [-h] [--host HOST] [--user USER]
options:
-h, --help show this help message and exit
database:
--host HOST
credentials の節が、ヘルプに出ていません。 --user は usage 行には現れますが、本体には database の節しかなく、credentials という見出しも --user の説明も表示されていません。
引数のパース自体は動くので、credentials.add_argument("--user") で足した --user は受け付けられます。しかしグループとして分けた意味は、3.13 の時点ですでに失われていました。だから parser の直下に並べても、ヘルプの見え方はむしろ正しくなります(credentials の節が出るようになります)。
そもそも argparse の引数グループは、ヘルプを平らな節に分けるだけで、子グループを階層表示する機能を持ちません。入れ子にした親子関係を --help に反映する方法は無いので、平坦化して失うものはありません。CLI で階層的な構造を見せたいなら、それは argparse の範囲外です(サブコマンド add_subparsers() など、別の仕組みを使います)。
Python 3.11 から、入れ子には DeprecationWarning: Nesting argument groups is deprecated. が出ていました(実測)。3.14 はそれを ValueError にしました。
グループの中に相互排他グループを作る形は、3.14 でも通ります
止まるのは add_argument_group() の入れ子です。引数グループの中に相互排他グループ(mutually exclusive group)を作るのは、3.14 でも正当です(実測)。
parser = argparse.ArgumentParser()
database = parser.add_argument_group("database")
mode = database.add_mutually_exclusive_group() # 3.14 でも通る
mode.add_argument("--read-only", action="store_true")
mode.add_argument("--read-write", action="store_true")
これはよくある正当なパターンで、禁止されていません。3.14 が ValueError で拒否するのは、次の 3 経路です(実測)。
| 呼び出し | 3.14 |
|---|---|
group.add_mutually_exclusive_group() | 通る(上の例) |
group.add_argument_group() | ValueError: argument groups cannot be nested |
mutex.add_argument_group() | ValueError: argument groups cannot be nested |
mutex.add_mutually_exclusive_group() | ValueError: mutually exclusive groups cannot be nested |
相互排他グループの中に相互排他グループを作る形も落ちます(別のメッセージです)。通るのは「引数グループの中に相互排他グループ」の 1 経路だけです。
予告は既定では見えないことがあります
DeprecationWarning が実際に表示されるかは、どこでグループを定義しているかで変わります。
- スクリプトを
python app.pyで直接実行し、その__main__の中でグループを定義していれば、-Wを付けなくても警告が出ます。 - パーサの構築を別のモジュール(
cli.pyなど)に切り出していると、既定のフィルタでは出ません。python -W always::DeprecationWarning app.pyで走らせる必要があります。
CLI ツールはパーサ構築を関数やモジュールに分けていることが多いので、後者になりがちです。3.14 に上げる前に確認したいなら、-W always::DeprecationWarning を付けて一度実行してください。
切り分け
parse_args()を呼ぶ前に落ちる → このエラーです。グループの定義時点で送出されます。add_argument_group()を入れ子にしている → これが原因です。両方をparser直下に移してください。- 相互排他グループを使っているのに落ちる → 引数グループの中の相互排他グループ自体は正当です。その相互排他グループからさらにグループ(
add_argument_group()でもadd_mutually_exclusive_group()でも)を作っていないか確認してください。どちらも落ちます。 - 平坦化したら CLI の階層表示が消えた → argparse の引数グループはもともとヘルプを平らな節に分けるだけで、階層表示の機能はありません。階層が要るならサブコマンド(
add_subparsers())など別の仕組みを使ってください。 - 警告が一度も出なかったのに 3.14 で落ちた → パーサ構築が
__main__の外にあります。-W always::DeprecationWarningを付けると出ます。 - 他の Python 3.14 のエラーを踏んだ → 同じ版境界の姉妹記事があります。functools.partial がクラス属性で壊れる は、同じく「触っていないコードが 3.14 で落ちる」型です。
検証環境
python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1(Python 3.14.6)、ネットワーク不要- 再現:
parser.add_argument_group()で作ったグループからadd_argument_group()を呼び、ValueError: argument groups cannot be nestedが出て終了コード 1 - 修正:両方のグループを
parserから作り、同じ Python 3.14.6 で終了コード 0(--userの値を出力)・シグネチャ消滅 - 3.13 では入れ子が通るが
--helpに子グループの節が出ないこと、group.add_mutually_exclusive_group()は 3.14 でも通ること、group.add_argument_group()とmutex.add_argument_group()はargument groups cannot be nested、mutex.add_mutually_exclusive_group()はmutually exclusive groups cannot be nestedになること、警告の表示が呼び出し元が__main__かどうかで変わることは、python:3.12-slim(3.12.13)・python:3.13-slim(3.13.14)・3.14.6 の 3 版でそれぞれ実行して確認しました - 3.12 と 3.13 の間、3.13 と 3.14 の間のパッチ版は測っていません