https://github.com/python/cpython/commit/35061259cc7064988586797f4cc0ba826188c70b
commit: 35061259cc7064988586797f4cc0ba826188c70b
branch: main
author: Hugo van Kemenade <[email protected]>
committer: hugovk <[email protected]>
date: 2026-05-04T08:44:37Z
summary:

gh-146609: Use argparse for colour help timeit CLI (#149334)

Co-authored-by: Stan Ulbrych <[email protected]>
Co-authored-by: Hugo van Kemenade <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst
M Lib/test/test_timeit.py
M Lib/timeit.py

diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py
index f41a8a41834b3b..81f1a9c97393d1 100644
--- a/Lib/test/test_timeit.py
+++ b/Lib/test/test_timeit.py
@@ -259,11 +259,12 @@ def run_main(self, seconds_per_increment=1.0, 
switches=None, timer=None):
         return s.getvalue()
 
     def test_main_bad_switch(self):
-        s = self.run_main(switches=['--bad-switch'])
-        self.assertEqual(s, dedent("""\
-            option --bad-switch not recognized
-            use -h/--help for command line help
-            """))
+        with captured_stderr() as error_stringio:
+            s = self.run_main(switches=["--bad-switch"])
+        self.assertEqual(s, "")
+        self.assertIn(
+            "unrecognized arguments: --bad-switch", error_stringio.getvalue()
+        )
 
     def test_main_seconds(self):
         s = self.run_main(seconds_per_increment=5.5)
@@ -301,10 +302,11 @@ def test_main_negative_reps(self):
         s = self.run_main(seconds_per_increment=60.0, switches=['-r-5'])
         self.assertEqual(s, "1 loop, best of 1: 60 sec per loop\n")
 
-    @unittest.skipIf(sys.flags.optimize >= 2, "need __doc__")
     def test_main_help(self):
         s = self.run_main(switches=['-h'])
-        self.assertEqual(s, timeit.__doc__)
+        self.assertIn("Tool for measuring execution time", s)
+        self.assertIn("-n", s)
+        self.assertIn("--number", s)
 
     def test_main_verbose(self):
         s = self.run_main(switches=['-v'])
@@ -353,10 +355,12 @@ def test_main_with_time_unit(self):
                 "100 loops, best of 5: 3e+03 usec per loop\n")
         # Test invalid unit input
         with captured_stderr() as error_stringio:
-            invalid = self.run_main(seconds_per_increment=0.003,
-                    switches=['-u', 'parsec'])
-        self.assertEqual(error_stringio.getvalue(),
-                    "Unrecognized unit. Please select nsec, usec, msec, or 
sec.\n")
+            invalid = self.run_main(
+                seconds_per_increment=0.003, switches=["-u", "parsec"]
+            )
+        self.assertIn(
+            "choose from nsec, usec, msec, sec", error_stringio.getvalue()
+        )
 
     def test_main_exception(self):
         with captured_stderr() as error_stringio:
diff --git a/Lib/timeit.py b/Lib/timeit.py
index f09ef43400ebd8..a897d9663c24e2 100644
--- a/Lib/timeit.py
+++ b/Lib/timeit.py
@@ -6,38 +6,6 @@
 
 Library usage: see the Timer class.
 
-Command line usage:
-    python timeit.py [-n N] [-r N] [-s S] [-p] [-h] [-t T] [--] [statement]
-
-Options:
-  -n/--number N: how many times to execute 'statement' (default: see below)
-  -r/--repeat N: how many times to repeat the timer (default 5)
-  -s/--setup S: statement to be executed once initially (default 'pass').
-                Execution time of this setup statement is NOT timed.
-  -p/--process: use time.process_time() (default is time.perf_counter())
-  -v/--verbose: print raw timing results; repeat for more digits precision
-  -u/--unit: set the output time unit (nsec, usec, msec, or sec)
-  -t/--target-time T: if --number is 0 the code will run until it
-                      takes *at least* this many seconds
-                      (default: 0.2)
-  -h/--help: print this usage message and exit
-  --: separate options from statement, use when statement starts with -
-  statement: statement to be timed (default 'pass')
-
-A multi-line statement may be given by specifying each line as a
-separate argument; indented lines are possible by enclosing an
-argument in quotes and using leading spaces.  Multiple -s options are
-treated similarly.
-
-If -n is not given, a suitable number of loops is calculated by trying
-increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the
-total time is at least --target-time seconds.
-
-Note: there is a certain baseline overhead associated with executing a
-pass statement.  It differs between versions.  The code here doesn't try
-to hide it, but you should be aware of it.  The baseline overhead can be
-measured by invoking the program without arguments.
-
 Classes:
 
     Timer
@@ -268,7 +236,7 @@ def main(args=None, *, _wrap_timer=None):
     is not None, it must be a callable that accepts a timer function
     and returns another timer function (used for unit testing).
     """
-    import getopt
+    import argparse
     if args is None:
         args = sys.argv[1:]
     import _colorize
@@ -276,54 +244,106 @@ def main(args=None, *, _wrap_timer=None):
     theme = _colorize.get_theme(force_color=colorize).timeit
     reset = theme.reset
 
+    epilog = """\
+A multi-line statement may be given by specifying each line as a
+separate argument; indented lines are possible by enclosing an
+argument in quotes and using leading spaces. Multiple `-s` options are
+treated similarly.
+
+If `-n` is not given, a suitable number of loops is calculated by trying
+increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the
+total time is at least `--target-time` seconds.
+
+Note: there is a certain baseline overhead associated with executing a
+pass statement. It differs between versions. The code here doesn't try
+to hide it, but you should be aware of it. The baseline overhead can be
+measured by invoking the program without arguments."""
+
+    parser = argparse.ArgumentParser(
+        prog="python -m timeit",
+        description="""\
+Tool for measuring execution time of small code snippets.
+
+This module avoids a number of common traps for measuring execution
+times. See also Tim Peters' introduction to the Algorithms chapter in
+the Python Cookbook, published by O'Reilly.
+
+Library usage: see the `Timer` class.""",
+        epilog=epilog,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument(
+        "-n",
+        "--number",
+        type=int,
+        default=0,
+        help="how many times to execute 'statement' (default: see below)",
+    )
+    parser.add_argument(
+        "-r",
+        "--repeat",
+        type=int,
+        default=default_repeat,
+        help="how many times to repeat the timer (default %(default)s)",
+    )
+    parser.add_argument(
+        "-s",
+        "--setup",
+        action="append",
+        default=[],
+        help="statement to be executed once initially. "
+        "Execution time of this setup statement is NOT timed. "
+        "(default 'pass')",
+    )
+    parser.add_argument(
+        "-p",
+        "--process",
+        action="store_true",
+        help="use time.process_time() (default is time.perf_counter())",
+    )
+    parser.add_argument(
+        "-t",
+        "--target-time",
+        type=float,
+        default=default_target_time,
+        help="if --number is 0 the code will run until it takes "
+        "at least this many seconds (default %(default)s)",
+    )
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        action="count",
+        default=0,
+        help="print raw timing results; repeat for more digits precision",
+    )
+    parser.add_argument(
+        "-u",
+        "--unit",
+        default=None,
+        choices=["nsec", "usec", "msec", "sec"],
+        help="set the output time unit",
+    )
+    parser.add_argument(
+        "statement",
+        nargs="*",
+        default=["pass"],
+        help="statement to be timed (default 'pass')",
+    )
     try:
-        opts, args = getopt.getopt(args, "n:u:s:r:pt:vh",
-                                   ["number=", "setup=", "repeat=",
-                                    "process", "target-time=",
-                                    "verbose", "unit=", "help"])
-    except getopt.error as err:
-        print(err)
-        print("use -h/--help for command line help")
-        return 2
-
-    timer = default_timer
-    stmt = "\n".join(args) or "pass"
-    number = 0  # auto-determine
-    target_time = default_target_time
-    setup = []
-    repeat = default_repeat
-    verbose = 0
-    time_unit = None
+        ns = parser.parse_args(args)
+    except SystemExit as e:
+        return e.code
+
+    timer = time.process_time if ns.process else default_timer
+    stmt = "\n".join(ns.statement) or "pass"
+    number = ns.number
+    target_time = ns.target_time
+    setup = "\n".join(ns.setup) or "pass"
+    repeat = max(ns.repeat, 1)
+    verbose = ns.verbose
+    time_unit = ns.unit
     units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0}
-    precision = 3
-    for o, a in opts:
-        if o in ("-n", "--number"):
-            number = int(a)
-        if o in ("-s", "--setup"):
-            setup.append(a)
-        if o in ("-u", "--unit"):
-            if a in units:
-                time_unit = a
-            else:
-                print("Unrecognized unit. Please select nsec, usec, msec, or 
sec.",
-                      file=sys.stderr)
-                return 2
-        if o in ("-r", "--repeat"):
-            repeat = int(a)
-            if repeat <= 0:
-                repeat = 1
-        if o in ("-p", "--process"):
-            timer = time.process_time
-        if o in ("-t", "--target-time"):
-            target_time = float(a)
-        if o in ("-v", "--verbose"):
-            if verbose:
-                precision += 1
-            verbose += 1
-        if o in ("-h", "--help"):
-            print(__doc__, end="")
-            return 0
-    setup = "\n".join(setup) or "pass"
+    precision = 3 + max(verbose - 1, 0)
 
     # Include the current directory, so that local imports work (sys.path
     # contains the directory of this script, rather than the current
diff --git 
a/Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst 
b/Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst
new file mode 100644
index 00000000000000..51fde3b42494ec
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst
@@ -0,0 +1 @@
+Use :mod:`argparse` for colour help :mod:`timeit` CLI. Patch by Hugo van 
Kemenade.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to