From 2b68b56328f2065703ce950c3368a83e3971756a Mon Sep 17 00:00:00 2001 From: jb2170 Date: Sun, 3 May 2026 16:50:45 +0100 Subject: [PATCH 1/8] argparse subnamespace: Add implementation for subnamespace as a str --- Lib/argparse.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 9bc3ea64431e52..3e6e91e690870b 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1322,6 +1322,8 @@ def add_parser(self, name, *, deprecated=False, **kwargs): if kwargs.get('prog') is None: kwargs['prog'] = '%s %s' % (self._prog_prefix, name) + subnamespace_name = kwargs.pop('subnamespace', None) + # set color if kwargs.get('color') is None: kwargs['color'] = self._color @@ -1348,6 +1350,11 @@ def add_parser(self, name, *, deprecated=False, **kwargs): parser._check_help(choice_action) self._name_parser_map[name] = parser + # add the subnamespace attribute to the parser if specified + # for nested namespaces + if subnamespace_name is not None: + setattr(parser, 'subnamespace', subnamespace_name) + # make parser available under aliases also for alias in aliases: self._name_parser_map[alias] = parser @@ -1390,8 +1397,17 @@ def __call__(self, parser, namespace, values, option_string=None): # in a new namespace object and then update the original # namespace for the relevant parts. subnamespace, arg_strings = subparser.parse_known_args(arg_strings, None) - for key, value in vars(subnamespace).items(): - setattr(namespace, key, value) + + # If a subnamespace name has been specified for the subparser + # then store the subparser's namespace within the parent namespace + # using that name. Otherwise update the parent namespace with the + # values from the subnamespace. + subnamespace_name = getattr(subparser, 'subnamespace', None) + if subnamespace_name is not None: + setattr(namespace, subnamespace_name, subnamespace) + else: + for key, value in vars(subnamespace).items(): + setattr(namespace, key, value) if arg_strings: if not hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR): From 71d1c357dcf1765d236ef7813344043065347bb7 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 5 May 2026 16:14:10 +0100 Subject: [PATCH 2/8] argparse subnamespace: Replace '-'s with '_'s in subnamespace destintation name Like in `_ActionsContainer._get_optional_kwargs`. It makes it much easier to access the subnamespaces by `x.y` instead of needing `getattr(x, 'y')` in the case that y (the subparser choice) has hyphens in it. --- Lib/argparse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/argparse.py b/Lib/argparse.py index 3e6e91e690870b..950d481c9278bc 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1404,6 +1404,7 @@ def __call__(self, parser, namespace, values, option_string=None): # values from the subnamespace. subnamespace_name = getattr(subparser, 'subnamespace', None) if subnamespace_name is not None: + subnamespace_name = subnamespace_name.replace('-', '_') setattr(namespace, subnamespace_name, subnamespace) else: for key, value in vars(subnamespace).items(): From 6ed63b3c4bf2449fabb83ca4f179d1a5f4be3492 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 5 May 2026 17:10:47 +0100 Subject: [PATCH 3/8] argparse subnamespace: Add tests I've tried to be in depth, and the tests are commented. Comments can be removed later if one wants, but they're in the git history here for the record. --- Lib/test/test_argparse.py | 211 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 88c1a21aa28551..9a4af4be5c9679 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -6164,6 +6164,217 @@ def test_equality_returns_notimplemented(self): self.assertIs(ns.__ne__(None), NotImplemented) +# ================== +# Subnamespace tests +# ================== + +class TestSubnamespace(TestCase): + + def test_single_subnamespace(self): + parser = argparse.ArgumentParser() + action = parser.add_subparsers(required=False, dest="action") + + parser_add = action.add_parser("add", subnamespace="add") + parser_add.add_argument("--to") + + parser_remove = action.add_parser("remove", subnamespace="remove") + parser_remove.add_argument("--from") + + # a root parser should not have 'subnamespace' attribute, + # as that attribute should only be set when using + # `_SubParsersAction.add_parser()` + self.assertNotHasAttr(parser, "subnamespace") + + # check subparser has 'subnamespace' attribute, + # that was set when calling `action.add_parser()` + self.assertHasAttr(parser_add, "subnamespace") + + # 'subnamespace' attribute is a string + self.assertIsInstance(parser_add.subnamespace, str) + + # check nesting of Namspaces works + args = parser.parse_args(["add"]) + self.assertEqual(args, argparse.Namespace( + action="add", add=argparse.Namespace( + to=None + ))) + + # test accessing of subnamespaces and args via `x.y` works + self.assertEqual(args.add.to, None) + + # check 'required=False' allows no subnamespaces to be created + args = parser.parse_args([]) + self.assertEqual(args, argparse.Namespace(action=None)) + + def test_double_subnamespace(self): + def add_address_args_inet(parser): + parser.add_argument("address") + parser.add_argument("port", type=int) + parser.add_argument("--use-proxy", action="store_true") + + def add_address_args_unix(parser): + parser.add_argument("path") + + parser = argparse.ArgumentParser() + parser.add_argument("--key-file") + action = parser.add_subparsers(required=True, dest="action") + + parser_bind = action.add_parser("bind", subnamespace="bind") + parser_bind.add_argument("--fork", action="store_true") + bind_family = parser_bind.add_subparsers(required=True, dest="family") + + parser_bind_inet = bind_family.add_parser("inet", subnamespace="inet") + add_address_args_inet(parser_bind_inet) + + parser_bind_unix = bind_family.add_parser("unix", subnamespace="unix") + add_address_args_unix(parser_bind_unix) + + parser_connect = action.add_parser("connect", subnamespace="connect") + connect_family = parser_connect.add_subparsers(required=True, dest="family") + + parser_connect_inet = connect_family.add_parser("inet", subnamespace="inet") + add_address_args_inet(parser_connect_inet) + + parser_connect_unix = connect_family.add_parser("unix", subnamespace="unix") + add_address_args_unix(parser_connect_unix) + + # check doubly-nested Namespaces work + # we assume if this test passes that we don't need to write + # redundant triply-nested etc Namespaces tests + args = parser.parse_args(["bind", "unix", "/foo/bar/socket"]) + self.assertEqual(args, argparse.Namespace( + key_file=None, + action="bind", bind=argparse.Namespace( + fork=False, + family="unix", unix=argparse.Namespace( + path="/foo/bar/socket" + )))) + + # we test with nested args with '-' in their name + # '--key-file' -> 'key_file' and '--use-proxy' -> 'use_proxy' + args = parser.parse_args(["--key-file", "/etc/key", "connect", + "inet", "127.0.0.1", "8000"]) + self.assertEqual(args, argparse.Namespace( + key_file="/etc/key", + action="connect", connect=argparse.Namespace( + family="inet", inet=argparse.Namespace( + address="127.0.0.1", port=8000, use_proxy=False + )))) + + # test accessing of nested args works when they have '_' in them + self.assertEqual(args.connect.inet.use_proxy, False) + + def test_mixed_some_subnamespace_some_not(self): + parser = argparse.ArgumentParser() + action = parser.add_subparsers(required=True, dest="action") + + parser_add = action.add_parser("add", subnamespace="add") + spec = parser_add.add_subparsers(required=True, dest="spec") + + parser_add_country = spec.add_parser("country", subnamespace=None) + parser_add_country.add_argument("country_name") + + parser_add_color = spec.add_parser("color", subnamespace="color") + parser_add_color.add_argument("name") + + # test that non-subnamespace parser arguments get parented to + # their parent Namespace, not the root Namespace + # ie make sure 'country_name' gets put under the 'add' Namespace + # not the root Namespace + args = parser.parse_args(["add", "country", "france"]) + self.assertEqual(args, argparse.Namespace( + action="add", add=argparse.Namespace( + spec="country", country_name="france" + ))) + + # test accessing of double subnamespaces works + # with non-subnamespace args + self.assertEqual(args.add.country_name, "france") + + # contrast above example with this one, where 'name' is + # parented under the 'add.color' Namespace + args = parser.parse_args(["add", "color", "blue"]) + self.assertEqual(args, argparse.Namespace( + action="add", add=argparse.Namespace( + spec="color", color=argparse.Namespace( + name="blue" + )))) + + # test accessing of double subnamespaces works, + # in particular check that we're not just getting a proxy + # or some descriptor chicanery; args genuinely are stored + # hierarchically + self.assertIsInstance(args.add.color, argparse.Namespace) + self.assertEqual(args.add.color.name, "blue") + + def test_exotic_subnamespace_names(self): + parser = argparse.ArgumentParser() + parser.add_argument("-f", help="fast-tracked order", action="store_true") + + choice = parser.add_subparsers(required=True, dest="choice") + + parser_0 = choice.add_parser("0", subnamespace="0", + help="number 0 menu item") + + parser_1 = choice.add_parser("1", subnamespace="1", + help="number 1 menu item") + + parser_True = choice.add_parser("True", subnamespace="True", + help="limited edition 'True' meal") + parser_True.add_argument("--deluxe", "-d", action="store_true") + + parser_double_cheeseburger = choice.add_parser("double-cheeseburger", + subnamespace="double-cheeseburger") + + parser_chicken_nuggets = choice.add_parser("chicken-nuggets", + subnamespace="chicken-nuggets") + parser_chicken_nuggets.add_argument("-f", help="with fries", + action="store_true") + + # test to observe '-' being replaced with '_' in + # subnamespace attribute name + args = parser.parse_args(["double-cheeseburger"]) + self.assertNotHasAttr(args, "double-cheeseburger") + self.assertHasAttr(args, "double_cheeseburger") + self.assertEqual(args, argparse.Namespace( + f=False, + choice="double-cheeseburger", double_cheeseburger=argparse.Namespace() + )) + + # test to observe two different `f` flags with different values + # something which is not possible without subnamespaces, + # and a key reason of why we're interested in subnamespaces + args = parser.parse_args(["chicken-nuggets", "-f"]) + self.assertEqual(args, argparse.Namespace( + f=False, + choice="chicken-nuggets", chicken_nuggets=argparse.Namespace( + f=True, + ))) + + # test to check that the order in which the `-f` flags appear + # doesn't lead to a last-write-wins situation; the `f`s are + # genuinely being parsed individually and are not overwriting + # each other in the `_SubParsersAction.__call__` stage + args = parser.parse_args(["-f", "chicken-nuggets"]) + self.assertEqual(args, argparse.Namespace( + f=True, + choice="chicken-nuggets", chicken_nuggets=argparse.Namespace( + f=False, + ))) + + # check that subnamespaces that aren't accessible by `x.y` notation + # are still accessible via getattr + args = parser.parse_args(["0"]) + self.assertHasAttr(args, "0") + self.assertEqual(getattr(args, "0"), argparse.Namespace()) + + # check that subparsers whose name are a Python keyword + # are acceptable and their subnamespaces are correctly stored + args = parser.parse_args(["True"]) + self.assertHasAttr(args, "True") + self.assertEqual(getattr(args, "True"), argparse.Namespace(deluxe=False)) + + # =================== # File encoding tests # =================== From 71058d214ad5cdfc854d4865167413b973b88a6e Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 5 May 2026 17:16:05 +0100 Subject: [PATCH 4/8] argparse subnamespace: Make 'subnamespace' explicit in signature of `_SubParsersAction.add_parser` Instead of it being mixed into kwargs, we keep 'subnamespace' separate like 'deprecated'. This makes it clearer that 'subnamespace' isn't intended as a parameter to be passed to the subparser constructor. --- Lib/argparse.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 950d481c9278bc..e3a034b26552ae 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1317,13 +1317,11 @@ def __init__(self, help=help, metavar=metavar) - def add_parser(self, name, *, deprecated=False, **kwargs): + def add_parser(self, name, *, deprecated=False, subnamespace=None, **kwargs): # set prog from the existing prefix if kwargs.get('prog') is None: kwargs['prog'] = '%s %s' % (self._prog_prefix, name) - subnamespace_name = kwargs.pop('subnamespace', None) - # set color if kwargs.get('color') is None: kwargs['color'] = self._color @@ -1352,8 +1350,8 @@ def add_parser(self, name, *, deprecated=False, **kwargs): # add the subnamespace attribute to the parser if specified # for nested namespaces - if subnamespace_name is not None: - setattr(parser, 'subnamespace', subnamespace_name) + if subnamespace is not None: + setattr(parser, 'subnamespace', subnamespace) # make parser available under aliases also for alias in aliases: From f62fbff68b217725ffcb71ac0aa7846a80d632d0 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 5 May 2026 17:20:30 +0100 Subject: [PATCH 5/8] argparse subnamespace: begin change from str to bool Commit to mark last version in which 'subnamespace' is a string indicating where a subNamespace should be stored in its parent Namespace. We're changing over to using a 'bool' instead, and letting the subnamespace destination be determined by the subparser name automatically. I don't need a git tag for this, just a commit. From 76edae9a877990a602b5482b2162f25aaf28e278 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 5 May 2026 18:11:47 +0100 Subject: [PATCH 6/8] argparse subnamespace: Change implementation and tests from str to bool --- Lib/argparse.py | 29 +++++++++++--------- Lib/test/test_argparse.py | 56 +++++++++++++++++---------------------- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index e3a034b26552ae..cc39a7c5b077fc 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1317,7 +1317,7 @@ def __init__(self, help=help, metavar=metavar) - def add_parser(self, name, *, deprecated=False, subnamespace=None, **kwargs): + def add_parser(self, name, *, deprecated=False, subnamespace=False, **kwargs): # set prog from the existing prefix if kwargs.get('prog') is None: kwargs['prog'] = '%s %s' % (self._prog_prefix, name) @@ -1348,10 +1348,10 @@ def add_parser(self, name, *, deprecated=False, subnamespace=None, **kwargs): parser._check_help(choice_action) self._name_parser_map[name] = parser - # add the subnamespace attribute to the parser if specified - # for nested namespaces - if subnamespace is not None: - setattr(parser, 'subnamespace', subnamespace) + # set the subnamespace attribute on the parser to determine + # whether parsed arguments should be stored in their own + # nested namespace or added to the parent parser's namespace + setattr(parser, 'subnamespace', subnamespace) # make parser available under aliases also for alias in aliases: @@ -1396,13 +1396,18 @@ def __call__(self, parser, namespace, values, option_string=None): # namespace for the relevant parts. subnamespace, arg_strings = subparser.parse_known_args(arg_strings, None) - # If a subnamespace name has been specified for the subparser - # then store the subparser's namespace within the parent namespace - # using that name. Otherwise update the parent namespace with the - # values from the subnamespace. - subnamespace_name = getattr(subparser, 'subnamespace', None) - if subnamespace_name is not None: - subnamespace_name = subnamespace_name.replace('-', '_') + # If the subparser's 'subnamespace' attribute is ``True`` + # then store the subparser's parsed arguments contained in + # their own namespace, nested within the parent namespace. + # The attribute name in the parent namespace at which the + # subparser's subnamespace is stored is the subparser's name, + # specified when using '.add_parser()', but with '-' replaced with '_' + # similar to how options are stored. + # + # Otherwise if 'subnamespace' is ``False`` then update + # the parent namespace with the values from the subnamespace. + if subparser.subnamespace: + subnamespace_name = parser_name.replace('-', '_') setattr(namespace, subnamespace_name, subnamespace) else: for key, value in vars(subnamespace).items(): diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 9a4af4be5c9679..2307de8aa66c78 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -6174,10 +6174,10 @@ def test_single_subnamespace(self): parser = argparse.ArgumentParser() action = parser.add_subparsers(required=False, dest="action") - parser_add = action.add_parser("add", subnamespace="add") + parser_add = action.add_parser("add", subnamespace=True) parser_add.add_argument("--to") - parser_remove = action.add_parser("remove", subnamespace="remove") + parser_remove = action.add_parser("remove", subnamespace=True) parser_remove.add_argument("--from") # a root parser should not have 'subnamespace' attribute, @@ -6189,8 +6189,8 @@ def test_single_subnamespace(self): # that was set when calling `action.add_parser()` self.assertHasAttr(parser_add, "subnamespace") - # 'subnamespace' attribute is a string - self.assertIsInstance(parser_add.subnamespace, str) + # 'subnamespace' attribute is a bool + self.assertIsInstance(parser_add.subnamespace, bool) # check nesting of Namspaces works args = parser.parse_args(["add"]) @@ -6207,36 +6207,30 @@ def test_single_subnamespace(self): self.assertEqual(args, argparse.Namespace(action=None)) def test_double_subnamespace(self): - def add_address_args_inet(parser): - parser.add_argument("address") - parser.add_argument("port", type=int) - parser.add_argument("--use-proxy", action="store_true") + inet = argparse.ArgumentParser(add_help=False) + inet.add_argument("address") + inet.add_argument("port", type=int) + inet.add_argument("--use-proxy", action="store_true") - def add_address_args_unix(parser): - parser.add_argument("path") + unix = argparse.ArgumentParser(add_help=False) + unix.add_argument("path") - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(prog="my-socat") parser.add_argument("--key-file") action = parser.add_subparsers(required=True, dest="action") - parser_bind = action.add_parser("bind", subnamespace="bind") + parser_bind = action.add_parser("bind", subnamespace=True) parser_bind.add_argument("--fork", action="store_true") bind_family = parser_bind.add_subparsers(required=True, dest="family") - parser_bind_inet = bind_family.add_parser("inet", subnamespace="inet") - add_address_args_inet(parser_bind_inet) - - parser_bind_unix = bind_family.add_parser("unix", subnamespace="unix") - add_address_args_unix(parser_bind_unix) + parser_bind_inet = bind_family.add_parser("inet", subnamespace=True, parents=[inet]) + parser_bind_unix = bind_family.add_parser("unix", subnamespace=True, parents=[unix]) - parser_connect = action.add_parser("connect", subnamespace="connect") + parser_connect = action.add_parser("connect", subnamespace=True) connect_family = parser_connect.add_subparsers(required=True, dest="family") - parser_connect_inet = connect_family.add_parser("inet", subnamespace="inet") - add_address_args_inet(parser_connect_inet) - - parser_connect_unix = connect_family.add_parser("unix", subnamespace="unix") - add_address_args_unix(parser_connect_unix) + parser_connect_inet = connect_family.add_parser("inet", subnamespace=True, parents=[inet]) + parser_connect_unix = connect_family.add_parser("unix", subnamespace=True, parents=[unix]) # check doubly-nested Namespaces work # we assume if this test passes that we don't need to write @@ -6268,13 +6262,13 @@ def test_mixed_some_subnamespace_some_not(self): parser = argparse.ArgumentParser() action = parser.add_subparsers(required=True, dest="action") - parser_add = action.add_parser("add", subnamespace="add") + parser_add = action.add_parser("add", subnamespace=True) spec = parser_add.add_subparsers(required=True, dest="spec") - parser_add_country = spec.add_parser("country", subnamespace=None) + parser_add_country = spec.add_parser("country", subnamespace=False) parser_add_country.add_argument("country_name") - parser_add_color = spec.add_parser("color", subnamespace="color") + parser_add_color = spec.add_parser("color", subnamespace=True) parser_add_color.add_argument("name") # test that non-subnamespace parser arguments get parented to @@ -6313,21 +6307,21 @@ def test_exotic_subnamespace_names(self): choice = parser.add_subparsers(required=True, dest="choice") - parser_0 = choice.add_parser("0", subnamespace="0", + parser_0 = choice.add_parser("0", subnamespace=True, help="number 0 menu item") - parser_1 = choice.add_parser("1", subnamespace="1", + parser_1 = choice.add_parser("1", subnamespace=True, help="number 1 menu item") - parser_True = choice.add_parser("True", subnamespace="True", + parser_True = choice.add_parser("True", subnamespace=True, help="limited edition 'True' meal") parser_True.add_argument("--deluxe", "-d", action="store_true") parser_double_cheeseburger = choice.add_parser("double-cheeseburger", - subnamespace="double-cheeseburger") + subnamespace=True) parser_chicken_nuggets = choice.add_parser("chicken-nuggets", - subnamespace="chicken-nuggets") + subnamespace=True) parser_chicken_nuggets.add_argument("-f", help="with fries", action="store_true") From 93daa90e310586a2f5bc300ce9bd3378be6b18a4 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 5 May 2026 20:52:22 +0100 Subject: [PATCH 7/8] argparse subnamespace: Add documentation of subnamespace feature --- Doc/library/argparse.rst | 73 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index db5fae2006678a..97aa359cfbb157 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -1716,8 +1716,9 @@ Subcommands :meth:`!add_subparsers` method. The :meth:`!add_subparsers` method is normally called with no arguments and returns a special action object. This object has a single method, :meth:`~_SubParsersAction.add_parser`, which takes a - command name and any :class:`!ArgumentParser` constructor arguments, and - returns an :class:`!ArgumentParser` object that can be modified as usual. + command name, optional deprecated_ and subnamespace_ flags, any + :class:`!ArgumentParser` constructor arguments, and returns an + :class:`!ArgumentParser` object that can be modified as usual. Description of parameters: @@ -1775,7 +1776,9 @@ Subcommands command line (and not any other subparsers). So in the example above, when the ``a`` command is specified, only the ``foo`` and ``bar`` attributes are present, and when the ``b`` command is specified, only the ``foo`` and - ``baz`` attributes are present. + ``baz`` attributes are present. If one wishes to store the the subparser's + attributes separate from the main parser's attributes, see the subnamespace_ + option of :meth:`~_SubParsersAction.add_parser`. Similarly, when a help message is requested from a subparser, only the help for that particular parser will be printed. The help message will not @@ -1896,7 +1899,8 @@ Subcommands .. method:: _SubParsersAction.add_parser(name, *, help=None, aliases=None, \ - deprecated=False, **kwargs) + deprecated=False, subnamespace=False, \ + **kwargs) Create and return a new :class:`ArgumentParser` object for the subcommand *name*. @@ -1925,12 +1929,73 @@ Subcommands chicken.py: warning: command 'fly' is deprecated Namespace() + .. _subnamespace: + + The *subnamespace* flag, if ``True``, tells the parent parser to + store the subparser's parsed arguments contained in + their own :class:`Namespace`, nested within the parent's :class:`!Namespace`. + The attribute name in the parent's namespace at which the + subparser's subnamespace is stored is the subparser's *name*, + but with underscores ``_`` replacing hyphens ``-`` + similar to dest_ in :meth:`ArgumentParser.add_argument`. + + This is useful for receiving parsed arguments hierarchically, mirroring the + hierarchical relation between a parser and its subparsers. For example:: + + >>> inet = argparse.ArgumentParser(add_help=False) + >>> inet.add_argument("address") + >>> inet.add_argument("port", type=int) + >>> + >>> unix = argparse.ArgumentParser(add_help=False) + >>> unix.add_argument("path") + >>> + >>> parser = argparse.ArgumentParser(prog='my-socat') + >>> action = parser.add_subparsers(required=True, dest="action") + >>> + >>> parser_bind = action.add_parser("bind", subnamespace=True) + >>> parser_bind.add_argument("--fork", action="store_true") + >>> bind_family = parser_bind.add_subparsers(required=True, dest="family") + >>> + >>> parser_bind_inet = bind_family.add_parser("inet", subnamespace=True, parents=[inet]) + >>> parser_bind_unix = bind_family.add_parser("unix", subnamespace=True, parents=[unix]) + >>> + >>> parser_connect = action.add_parser("connect", subnamespace=True) + >>> connect_family = parser_connect.add_subparsers(required=True, dest="family") + >>> + >>> parser_connect_inet = connect_family.add_parser("inet", subnamespace=True, parents=[inet]) + >>> parser_connect_unix = connect_family.add_parser("unix", subnamespace=True, parents=[unix]) + >>> + >>> args = parser.parse_args(["bind", "unix", "/foo/bar/socket"]) + >>> args + Namespace(action='bind', bind=Namespace(fork=False, family='unix', unix=Namespace(path='/foo/bar/socket'))) + + This is also very useful when one has arguments in subparsers whose + ``dest`` conflict with those of the parent parser's arguments, and one + wishes to faithfully distinguish between the two. For example:: + + >>> parser = argparse.ArgumentParser(prog='restaurant.py') + >>> parser.add_argument('-f', help='fast-tracked order', action='store_true') + >>> meals = parser.add_subparsers(dest='meal') + >>> + >>> parser_nuggets = meals.add_parser('chicken-nuggets', subnamespace=True) + >>> parser_nuggets.add_argument('-f', help='with fries', action='store_true') + >>> + >>> parser_salad = meals.add_parser('caesar-salad', subnamespace=True) + >>> parser_salad.add_argument('-f', help='fresh', action='store_true') + >>> + >>> args = parser.parse_args(['chicken-nuggets', '-f']) + >>> args + Namespace(f=False, meal='chicken-nuggets', chicken_nuggets=Namespace(f=True)) + All other keyword arguments are passed directly to the :class:`!ArgumentParser` constructor. .. versionadded:: 3.13 Added the *deprecated* parameter. + .. versionadded:: next + Added the *subnamespace* parameter. + FileType objects ^^^^^^^^^^^^^^^^ From a879304d4eb0a630ca1efd3fa0370a2af714a2b7 Mon Sep 17 00:00:00 2001 From: jb2170 Date: Tue, 5 May 2026 18:25:05 +0100 Subject: [PATCH 8/8] argparse subnamespace: Add NEWS and whatsnew sections --- Doc/whatsnew/3.15.rst | 6 ++++++ .../Library/2026-05-02-11-22-02.gh-issue-103639.l9hA7f.rst | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-05-02-11-22-02.gh-issue-103639.l9hA7f.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 2ca28378e6ef73..8539bb64ce126b 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -835,6 +835,12 @@ argparse double backticks (RST inline-literal style). (Contributed by Hugo van Kemenade in :gh:`149375`.) +* Added ``subnamespace`` keyword-only flag to + :meth:`argparse._SubParsersAction.add_parser` to allow nested + :class:`argparse.Namespace`\ s, which correspond with the hierarchical + nature of subparsers. By default ``subnamespace`` is ``False`` for + backwards compatibility. + array ----- diff --git a/Misc/NEWS.d/next/Library/2026-05-02-11-22-02.gh-issue-103639.l9hA7f.rst b/Misc/NEWS.d/next/Library/2026-05-02-11-22-02.gh-issue-103639.l9hA7f.rst new file mode 100644 index 00000000000000..0eb0f779332035 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-02-11-22-02.gh-issue-103639.l9hA7f.rst @@ -0,0 +1,5 @@ +Add ``subnamespace`` keyword-only flag to +:meth:`argparse._SubParsersAction.add_parser` to allow nested +:class:`argparse.Namespace`\ s, which correspond with the hierarchical +nature of subparsers. By default ``subnamespace`` is ``False`` for +backwards compatibility.