diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index f5ac965d3cfedf4..cf9b0e43646ec60 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -291,6 +291,16 @@ def _wrap_strftime(object, format, timetuple): push('{:04}'.format(year)) if ch == 'F': push('-{:02}-{:02}'.format(*timetuple[1:3])) + elif ch == '-': + if i < n: + next_ch = format[i] + i += 1 + if next_ch not in 'dmHIMSjUWVy': + push('%%-' + next_ch) + else: + push('%-' + next_ch) + else: + push('%-') else: push('%') push(ch) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 09a0ef5d223d2b6..6d19a420a11fcb5 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1598,6 +1598,15 @@ def test_strftime(self): self.assertEqual(t.strftime(""), "") # SF bug #761337 self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 + # unsupported %-format specifiers are passed through unchanged. + self.assertEqual(t.strftime("%-1"), "%-1") + self.assertEqual(t.strftime("%--"), "%--") + self.assertEqual(t.strftime("%-#"), "%-#") + # non-ASCII specifiers are unsupported too: echo without narrowing. + self.assertEqual(t.strftime("%-é"), "%-é") + self.assertEqual(t.strftime("%-Ť"), "%-Ť") # low byte aliases 'd' + self.assertEqual(t.strftime("%-中"), "%-中") + self.assertRaises(TypeError, t.strftime) # needs an arg self.assertRaises(TypeError, t.strftime, "one", "two") # too many args self.assertRaises(TypeError, t.strftime, 42) # arg wrong type diff --git a/Misc/NEWS.d/next/Library/2026-10-21-02-40-00.gh-issue-137165.QXa3Pg.rst b/Misc/NEWS.d/next/Library/2026-10-21-02-40-00.gh-issue-137165.QXa3Pg.rst new file mode 100644 index 000000000000000..713c66b59e72a5e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-21-02-40-00.gh-issue-137165.QXa3Pg.rst @@ -0,0 +1,4 @@ +Unify :meth:`datetime.date.strftime`, :meth:`datetime.time.strftime` and +:meth:`datetime.datetime.strftime` behavior for unsupported non-zero-padded +(``%-``) format specifiers across platforms: unknown specifiers are now echoed +back literally instead of raising :exc:`ValueError` or returning partial output. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 511b444037ceac7..8ed0ccfb86d1d3f 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -2047,6 +2047,23 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } continue; } + else if (ch == '-' && i < flen) { + Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i); + i++; + + /* Only ASCII can name a conversion: strchr() would narrow + anything else to a byte, so echo it back literally. */ + if (next_ch > 127 || + strchr("dmHIMSjUWVy", (int)next_ch) == NULL) { + replacement = PyUnicode_FromFormat("%%%%-%c", (int)next_ch); + if (replacement == NULL) { + goto Error; + } + } + else { + continue; + } + } else { /* percent followed by something else */ continue;