gh-83648: Support deprecation of options, arguments and subcommands in argparse (GH-114086)

diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst
index 1395d45..952643a 100644
--- a/Doc/library/argparse.rst
+++ b/Doc/library/argparse.rst
@@ -777,6 +777,8 @@
    * dest_ - The name of the attribute to be added to the object returned by
      :meth:`parse_args`.
 
+   * deprecated_ - Whether or not use of the argument is deprecated.
+
 The following sections describe how each of these are used.
 
 
@@ -1439,6 +1441,34 @@
    >>> parser.parse_args('--foo XXX'.split())
    Namespace(bar='XXX')
 
+
+.. _deprecated:
+
+deprecated
+^^^^^^^^^^
+
+During a project's lifetime, some arguments may need to be removed from the
+command line. Before removing them, you should inform
+your users that the arguments are deprecated and will be removed.
+The ``deprecated`` keyword argument of
+:meth:`~ArgumentParser.add_argument`, which defaults to ``False``,
+specifies if the argument is deprecated and will be removed
+in the future.
+For arguments, if ``deprecated`` is ``True``, then a warning will be
+printed to standard error when the argument is used::
+
+   >>> import argparse
+   >>> parser = argparse.ArgumentParser(prog='snake.py')
+   >>> parser.add_argument('--legs', default=0, type=int, deprecated=True)
+   >>> parser.parse_args([])
+   Namespace(legs=0)
+   >>> parser.parse_args(['--legs', '4'])  # doctest: +SKIP
+   snake.py: warning: option '--legs' is deprecated
+   Namespace(legs=4)
+
+.. versionchanged:: 3.13
+
+
 Action classes
 ^^^^^^^^^^^^^^
 
@@ -1842,7 +1872,8 @@
 
        {foo,bar}   additional help
 
-   Furthermore, ``add_parser`` supports an additional ``aliases`` argument,
+   Furthermore, :meth:`~_SubParsersAction.add_parser` supports an additional
+   *aliases* argument,
    which allows multiple strings to refer to the same subparser. This example,
    like ``svn``, aliases ``co`` as a shorthand for ``checkout``::
 
@@ -1853,6 +1884,20 @@
      >>> parser.parse_args(['co', 'bar'])
      Namespace(foo='bar')
 
+   :meth:`~_SubParsersAction.add_parser` supports also an additional
+   *deprecated* argument, which allows to deprecate the subparser.
+
+      >>> import argparse
+      >>> parser = argparse.ArgumentParser(prog='chicken.py')
+      >>> subparsers = parser.add_subparsers()
+      >>> run = subparsers.add_parser('run')
+      >>> fly = subparsers.add_parser('fly', deprecated=True)
+      >>> parser.parse_args(['fly'])  # doctest: +SKIP
+      chicken.py: warning: command 'fly' is deprecated
+      Namespace()
+
+   .. versionadded:: 3.13
+
    One particularly effective way of handling sub-commands is to combine the use
    of the :meth:`add_subparsers` method with calls to :meth:`set_defaults` so
    that each subparser knows which Python function it should execute.  For
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 9bac36b..5e5f1e2 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -169,6 +169,15 @@
 Improved Modules
 ================
 
+argparse
+--------
+
+* Add parameter *deprecated* in methods
+  :meth:`~argparse.ArgumentParser.add_argument` and :meth:`!add_parser`
+  which allows to deprecate command-line options, positional arguments and
+  subcommands.
+  (Contributed by Serhiy Storchaka in :gh:`83648`).
+
 array
 -----
 
diff --git a/Lib/argparse.py b/Lib/argparse.py
index 2131d72..04ee3b1 100644
--- a/Lib/argparse.py
+++ b/Lib/argparse.py
@@ -843,7 +843,8 @@ def __init__(self,
                  choices=None,
                  required=False,
                  help=None,
-                 metavar=None):
+                 metavar=None,
+                 deprecated=False):
         self.option_strings = option_strings
         self.dest = dest
         self.nargs = nargs
@@ -854,6 +855,7 @@ def __init__(self,
         self.required = required
         self.help = help
         self.metavar = metavar
+        self.deprecated = deprecated
 
     def _get_kwargs(self):
         names = [
@@ -867,6 +869,7 @@ def _get_kwargs(self):
             'required',
             'help',
             'metavar',
+            'deprecated',
         ]
         return [(name, getattr(self, name)) for name in names]
 
@@ -889,7 +892,8 @@ def __init__(self,
                  choices=_deprecated_default,
                  required=False,
                  help=None,
-                 metavar=_deprecated_default):
+                 metavar=_deprecated_default,
+                 deprecated=False):
 
         _option_strings = []
         for option_string in option_strings:
@@ -927,7 +931,8 @@ def __init__(self,
             choices=choices,
             required=required,
             help=help,
-            metavar=metavar)
+            metavar=metavar,
+            deprecated=deprecated)
 
 
     def __call__(self, parser, namespace, values, option_string=None):
@@ -950,7 +955,8 @@ def __init__(self,
                  choices=None,
                  required=False,
                  help=None,
-                 metavar=None):
+                 metavar=None,
+                 deprecated=False):
         if nargs == 0:
             raise ValueError('nargs for store actions must be != 0; if you '
                              'have nothing to store, actions such as store '
@@ -967,7 +973,8 @@ def __init__(self,
             choices=choices,
             required=required,
             help=help,
-            metavar=metavar)
+            metavar=metavar,
+            deprecated=deprecated)
 
     def __call__(self, parser, namespace, values, option_string=None):
         setattr(namespace, self.dest, values)
@@ -982,7 +989,8 @@ def __init__(self,
                  default=None,
                  required=False,
                  help=None,
-                 metavar=None):
+                 metavar=None,
+                 deprecated=False):
         super(_StoreConstAction, self).__init__(
             option_strings=option_strings,
             dest=dest,
@@ -990,7 +998,8 @@ def __init__(self,
             const=const,
             default=default,
             required=required,
-            help=help)
+            help=help,
+            deprecated=deprecated)
 
     def __call__(self, parser, namespace, values, option_string=None):
         setattr(namespace, self.dest, self.const)
@@ -1003,14 +1012,16 @@ def __init__(self,
                  dest,
                  default=False,
                  required=False,
-                 help=None):
+                 help=None,
+                 deprecated=False):
         super(_StoreTrueAction, self).__init__(
             option_strings=option_strings,
             dest=dest,
             const=True,
-            default=default,
+            deprecated=deprecated,
             required=required,
-            help=help)
+            help=help,
+            default=default)
 
 
 class _StoreFalseAction(_StoreConstAction):
@@ -1020,14 +1031,16 @@ def __init__(self,
                  dest,
                  default=True,
                  required=False,
-                 help=None):
+                 help=None,
+                 deprecated=False):
         super(_StoreFalseAction, self).__init__(
             option_strings=option_strings,
             dest=dest,
             const=False,
             default=default,
             required=required,
-            help=help)
+            help=help,
+            deprecated=deprecated)
 
 
 class _AppendAction(Action):
@@ -1042,7 +1055,8 @@ def __init__(self,
                  choices=None,
                  required=False,
                  help=None,
-                 metavar=None):
+                 metavar=None,
+                 deprecated=False):
         if nargs == 0:
             raise ValueError('nargs for append actions must be != 0; if arg '
                              'strings are not supplying the value to append, '
@@ -1059,7 +1073,8 @@ def __init__(self,
             choices=choices,
             required=required,
             help=help,
-            metavar=metavar)
+            metavar=metavar,
+            deprecated=deprecated)
 
     def __call__(self, parser, namespace, values, option_string=None):
         items = getattr(namespace, self.dest, None)
@@ -1077,7 +1092,8 @@ def __init__(self,
                  default=None,
                  required=False,
                  help=None,
-                 metavar=None):
+                 metavar=None,
+                 deprecated=False):
         super(_AppendConstAction, self).__init__(
             option_strings=option_strings,
             dest=dest,
@@ -1086,7 +1102,8 @@ def __init__(self,
             default=default,
             required=required,
             help=help,
-            metavar=metavar)
+            metavar=metavar,
+            deprecated=deprecated)
 
     def __call__(self, parser, namespace, values, option_string=None):
         items = getattr(namespace, self.dest, None)
@@ -1102,14 +1119,16 @@ def __init__(self,
                  dest,
                  default=None,
                  required=False,
-                 help=None):
+                 help=None,
+                 deprecated=False):
         super(_CountAction, self).__init__(
             option_strings=option_strings,
             dest=dest,
             nargs=0,
             default=default,
             required=required,
-            help=help)
+            help=help,
+            deprecated=deprecated)
 
     def __call__(self, parser, namespace, values, option_string=None):
         count = getattr(namespace, self.dest, None)
@@ -1124,13 +1143,15 @@ def __init__(self,
                  option_strings,
                  dest=SUPPRESS,
                  default=SUPPRESS,
-                 help=None):
+                 help=None,
+                 deprecated=False):
         super(_HelpAction, self).__init__(
             option_strings=option_strings,
             dest=dest,
             default=default,
             nargs=0,
-            help=help)
+            help=help,
+            deprecated=deprecated)
 
     def __call__(self, parser, namespace, values, option_string=None):
         parser.print_help()
@@ -1144,7 +1165,8 @@ def __init__(self,
                  version=None,
                  dest=SUPPRESS,
                  default=SUPPRESS,
-                 help="show program's version number and exit"):
+                 help="show program's version number and exit",
+                 deprecated=False):
         super(_VersionAction, self).__init__(
             option_strings=option_strings,
             dest=dest,
@@ -1188,6 +1210,7 @@ def __init__(self,
         self._parser_class = parser_class
         self._name_parser_map = {}
         self._choices_actions = []
+        self._deprecated = set()
 
         super(_SubParsersAction, self).__init__(
             option_strings=option_strings,
@@ -1198,7 +1221,7 @@ def __init__(self,
             help=help,
             metavar=metavar)
 
-    def add_parser(self, name, **kwargs):
+    def add_parser(self, name, *, deprecated=False, **kwargs):
         # set prog from the existing prefix
         if kwargs.get('prog') is None:
             kwargs['prog'] = '%s %s' % (self._prog_prefix, name)
@@ -1226,6 +1249,10 @@ def add_parser(self, name, **kwargs):
         for alias in aliases:
             self._name_parser_map[alias] = parser
 
+        if deprecated:
+            self._deprecated.add(name)
+            self._deprecated.update(aliases)
+
         return parser
 
     def _get_subactions(self):
@@ -1241,13 +1268,17 @@ def __call__(self, parser, namespace, values, option_string=None):
 
         # select the parser
         try:
-            parser = self._name_parser_map[parser_name]
+            subparser = self._name_parser_map[parser_name]
         except KeyError:
             args = {'parser_name': parser_name,
                     'choices': ', '.join(self._name_parser_map)}
             msg = _('unknown parser %(parser_name)r (choices: %(choices)s)') % args
             raise ArgumentError(self, msg)
 
+        if parser_name in self._deprecated:
+            parser._warning(_("command '%(parser_name)s' is deprecated") %
+                            {'parser_name': parser_name})
+
         # parse all the remaining options into the namespace
         # store any unrecognized options on the object, so that the top
         # level parser can decide what to do with them
@@ -1255,7 +1286,7 @@ def __call__(self, parser, namespace, values, option_string=None):
         # In case this subparser defines new defaults, we parse them
         # in a new namespace object and then update the original
         # namespace for the relevant parts.
-        subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
+        subnamespace, arg_strings = subparser.parse_known_args(arg_strings, None)
         for key, value in vars(subnamespace).items():
             setattr(namespace, key, value)
 
@@ -1975,6 +2006,7 @@ def _parse_known_args(self, arg_strings, namespace):
         # converts arg strings to the appropriate and then takes the action
         seen_actions = set()
         seen_non_default_actions = set()
+        warned = set()
 
         def take_action(action, argument_strings, option_string=None):
             seen_actions.add(action)
@@ -2070,6 +2102,10 @@ def consume_optional(start_index):
             # the Optional's string args stopped
             assert action_tuples
             for action, args, option_string in action_tuples:
+                if action.deprecated and option_string not in warned:
+                    self._warning(_("option '%(option)s' is deprecated") %
+                                  {'option': option_string})
+                    warned.add(option_string)
                 take_action(action, args, option_string)
             return stop
 
@@ -2089,6 +2125,10 @@ def consume_positionals(start_index):
             for action, arg_count in zip(positionals, arg_counts):
                 args = arg_strings[start_index: start_index + arg_count]
                 start_index += arg_count
+                if args and action.deprecated and action.dest not in warned:
+                    self._warning(_("argument '%(argument_name)s' is deprecated") %
+                                  {'argument_name': action.dest})
+                    warned.add(action.dest)
                 take_action(action, args)
 
             # slice off the Positionals that we just parsed and return the
@@ -2650,3 +2690,7 @@ def error(self, message):
         self.print_usage(_sys.stderr)
         args = {'prog': self.prog, 'message': message}
         self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
+
+    def _warning(self, message):
+        args = {'prog': self.prog, 'message': message}
+        self._print_message(_('%(prog)s: warning: %(message)s\n') % args, _sys.stderr)
diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py
index d1f3d40..86d6e81 100644
--- a/Lib/test/test_argparse.py
+++ b/Lib/test/test_argparse.py
@@ -5099,7 +5099,8 @@ def test_optional(self):
         string = (
             "Action(option_strings=['--foo', '-a', '-b'], dest='b', "
             "nargs='+', const=None, default=42, type='int', "
-            "choices=[1, 2, 3], required=False, help='HELP', metavar='METAVAR')")
+            "choices=[1, 2, 3], required=False, help='HELP', "
+            "metavar='METAVAR', deprecated=False)")
         self.assertStringEqual(option, string)
 
     def test_argument(self):
@@ -5116,7 +5117,8 @@ def test_argument(self):
         string = (
             "Action(option_strings=[], dest='x', nargs='?', "
             "const=None, default=2.5, type=%r, choices=[0.5, 1.5, 2.5], "
-            "required=True, help='H HH H', metavar='MV MV MV')" % float)
+            "required=True, help='H HH H', metavar='MV MV MV', "
+            "deprecated=False)" % float)
         self.assertStringEqual(argument, string)
 
     def test_namespace(self):
@@ -5308,6 +5310,139 @@ def spam(string_to_convert):
         args = parser.parse_args('--foo spam!'.split())
         self.assertEqual(NS(foo='foo_converted'), args)
 
+
+# ==============================================
+# Check that deprecated arguments output warning
+# ==============================================
+
+class TestDeprecatedArguments(TestCase):
+
+    def test_deprecated_option(self):
+        parser = argparse.ArgumentParser()
+        parser.add_argument('-f', '--foo', deprecated=True)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['--foo', 'spam'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: option '--foo' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['-f', 'spam'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: option '-f' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['--foo', 'spam', '-f', 'ham'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: option '--foo' is deprecated")
+        self.assertRegex(stderr, "warning: option '-f' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 2)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['--foo', 'spam', '--foo', 'ham'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: option '--foo' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+    def test_deprecated_boolean_option(self):
+        parser = argparse.ArgumentParser()
+        parser.add_argument('-f', '--foo', action=argparse.BooleanOptionalAction, deprecated=True)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['--foo'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: option '--foo' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['-f'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: option '-f' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['--no-foo'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: option '--no-foo' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['--foo', '--no-foo'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: option '--foo' is deprecated")
+        self.assertRegex(stderr, "warning: option '--no-foo' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 2)
+
+    def test_deprecated_arguments(self):
+        parser = argparse.ArgumentParser()
+        parser.add_argument('foo', nargs='?', deprecated=True)
+        parser.add_argument('bar', nargs='?', deprecated=True)
+
+        with captured_stderr() as stderr:
+            parser.parse_args([])
+        stderr = stderr.getvalue()
+        self.assertEqual(stderr.count('is deprecated'), 0)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['spam'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: argument 'foo' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['spam', 'ham'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: argument 'foo' is deprecated")
+        self.assertRegex(stderr, "warning: argument 'bar' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 2)
+
+    def test_deprecated_varargument(self):
+        parser = argparse.ArgumentParser()
+        parser.add_argument('foo', nargs='*', deprecated=True)
+
+        with captured_stderr() as stderr:
+            parser.parse_args([])
+        stderr = stderr.getvalue()
+        self.assertEqual(stderr.count('is deprecated'), 0)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['spam'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: argument 'foo' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['spam', 'ham'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: argument 'foo' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+    def test_deprecated_subparser(self):
+        parser = argparse.ArgumentParser()
+        subparsers = parser.add_subparsers()
+        subparsers.add_parser('foo', aliases=['baz'], deprecated=True)
+        subparsers.add_parser('bar')
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['bar'])
+        stderr = stderr.getvalue()
+        self.assertEqual(stderr.count('is deprecated'), 0)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['foo'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: command 'foo' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+        with captured_stderr() as stderr:
+            parser.parse_args(['baz'])
+        stderr = stderr.getvalue()
+        self.assertRegex(stderr, "warning: command 'baz' is deprecated")
+        self.assertEqual(stderr.count('is deprecated'), 1)
+
+
 # ==================================================================
 # Check semantics regarding the default argument and type conversion
 # ==================================================================
diff --git a/Misc/NEWS.d/next/Library/2024-01-15-20-21-33.gh-issue-83648.HzD_fY.rst b/Misc/NEWS.d/next/Library/2024-01-15-20-21-33.gh-issue-83648.HzD_fY.rst
new file mode 100644
index 0000000..bd3e27b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-01-15-20-21-33.gh-issue-83648.HzD_fY.rst
@@ -0,0 +1,2 @@
+Support deprecation of options, positional arguments and subcommands in
+:mod:`argparse`.