https://github.com/python/cpython/commit/98169145419470d243231c5d4992d006a971a8e3
commit: 98169145419470d243231c5d4992d006a971a8e3
branch: main
author: Vasiliy Kiryanov <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-09-21T21:01:23+01:00
summary:

gh-141540: Quote unconverted data in `strptime` error messages (#157832)

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

files:
A Misc/NEWS.d/next/Library/2026-09-19-20-31-58.gh-issue-141540.phc605.rst
M Lib/_strptime.py
M Lib/test/test_strptime.py

diff --git a/Lib/_strptime.py b/Lib/_strptime.py
index 59ac96745aa15e2..3311ec888714696 100644
--- a/Lib/_strptime.py
+++ b/Lib/_strptime.py
@@ -564,15 +564,15 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
                 del err
                 bad_directive = bad_directive.replace('\\s', '')
                 if not bad_directive:
-                    raise ValueError("stray %% in format '%s'" % format) from 
None
+                    raise ValueError(f"stray % in format {format!r}") from None
                 bad_directive = bad_directive.replace('\\', '', 1)
-                raise ValueError("'%s' is a bad directive in format '%s'" %
-                                    (bad_directive, format)) from None
+                raise ValueError(f"{bad_directive!r} is a bad directive "
+                                 f"in format {format!r}") from None
             _regex_cache[format] = format_regex
     found = format_regex.match(data_string)
     if not found:
-        raise ValueError("time data %r does not match format %r" %
-                         (data_string, format))
+        raise ValueError(f"time data {data_string!r} does not match "
+                         f"format {format!r}")
     if len(data_string) != found.end():
         rest = data_string[found.end():]
         # Specific check for '%:z' directive
@@ -582,9 +582,9 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
             and rest[0] != ":"
         ):
             raise ValueError(
-                f"Missing colon in %:z before '{rest}', got '{data_string}'"
+                f"Missing colon in %:z before {rest!r}, got {data_string!r}"
             )
-        raise ValueError("unconverted data remains: %s" % rest)
+        raise ValueError(f"unconverted data remains: {rest!r}")
 
     iso_year = year = None
     month = day = 1
@@ -700,7 +700,7 @@ def parse_int(s):
                         z = z[:3] + z[4:]
                         if len(z) > 5:
                             if z[5] != ':':
-                                msg = f"Inconsistent use of : in 
{found_dict[group_key]}"
+                                msg = f"Inconsistent use of : in 
{found_dict[group_key]!r}"
                                 raise ValueError(msg)
                             z = z[:5] + z[6:]
                     hours = int(z[1:3])
diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py
index e95cc6db170e243..d70837ea63f5474 100644
--- a/Lib/test/test_strptime.py
+++ b/Lib/test/test_strptime.py
@@ -236,7 +236,8 @@ def test_ValueError(self):
             directive = bad_format[1:].rstrip()
             with (self.subTest(format=bad_format),
                   self.assertRaisesRegex(ValueError,
-                    f"'{re.escape(directive)}' is a bad directive in format 
")):
+                    f"{re.escape(repr(directive))} is a bad directive "
+                    f"in format ")):
                 _strptime._strptime_time("2005", bad_format)
 
         msg_week_no_year_or_weekday = r"ISO week directive '%V' must be used 
with " \
@@ -303,6 +304,11 @@ def test_unconverteddata(self):
         # Check ValueError is raised when there is unconverted data
         self.assertRaises(ValueError, _strptime._strptime_time, "10 12", "%m")
 
+        # gh-141540: a trailing newline must be visible in the message
+        with self.assertRaisesRegex(ValueError,
+                                    r"unconverted data remains: '\\n'"):
+            _strptime._strptime_time("2001-02-03\n", "%Y-%m-%d")
+
     def roundtrip(self, fmt, position, time_tuple=None):
         """Helper fxn in testing."""
         if time_tuple is None:
@@ -451,7 +457,8 @@ def test_bad_offset(self):
 
         with self.assertRaises(ValueError) as err:
             _strptime._strptime("-01:3030", "%z")
-        self.assertEqual("Inconsistent use of : in -01:3030", 
str(err.exception))
+        self.assertEqual("Inconsistent use of : in '-01:3030'",
+                         str(err.exception))
         with self.assertRaises(ValueError) as err:
             _strptime._strptime("-01:3030", "%:z")
         self.assertEqual("Missing colon in %:z before '30', got '-01:3030'",
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-19-20-31-58.gh-issue-141540.phc605.rst 
b/Misc/NEWS.d/next/Library/2026-09-19-20-31-58.gh-issue-141540.phc605.rst
new file mode 100644
index 000000000000000..1b38ffd26ea20c3
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-19-20-31-58.gh-issue-141540.phc605.rst
@@ -0,0 +1,3 @@
+Quote the unconverted data and the format in the error messages of
+:func:`time.strptime` and :meth:`datetime.datetime.strptime` so that
+whitespace such as a trailing newline is visible.

_______________________________________________
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