Skip to content

Commit 03fbca3

Browse files
committed
Allow datetime.isoformat to use "Z" UTC designator
Add an optional parameter to datetime.isoformat() and time.isoformat() called use_utc_designator. If provided and True and the object is associated with a tzinfo and its tzname is exactly "UTC", the formatted UTC offset will be "Z" rather than an offset from UTC.
1 parent 3751b6b commit 03fbca3

5 files changed

Lines changed: 159 additions & 24 deletions

File tree

‎Doc/library/datetime.rst‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,7 +1409,7 @@ Instance methods:
14091409
and ``weekday``. The same as ``self.date().isocalendar()``.
14101410

14111411

1412-
.. method:: datetime.isoformat(sep='T', timespec='auto')
1412+
.. method:: datetime.isoformat(sep='T', timespec='auto', use_utc_designator=False)
14131413

14141414
Return a string representing the date and time in ISO 8601 format:
14151415

@@ -1473,9 +1473,22 @@ Instance methods:
14731473
>>> dt.isoformat(timespec='microseconds')
14741474
'2015-01-01T12:30:59.000000'
14751475

1476+
If the optional argument *use_utc_designator* is set to :const:`True` and
1477+
:meth:`tzname` returns exactly ``"UTC"``, then "Z" will be given as the UTC
1478+
offset in the formatted string::
1479+
1480+
>>> from datetime import datetime, timezone
1481+
>>> dt = datetime(2022, 3, 21, 12, 30, 59, tzinfo=timezone.utc)
1482+
>>> dt.isoformat()
1483+
'2022-03-21T12:30:59+00:00'
1484+
>>> dt.isoformat(use_utc_designator=True)
1485+
'2022-03-21T12:30:59Z'
1486+
14761487
.. versionadded:: 3.6
14771488
Added the *timespec* argument.
14781489

1490+
.. versionadded:: 3.11
1491+
Added the *use_utc_designator* argument.
14791492

14801493
.. method:: datetime.__str__()
14811494

@@ -1797,7 +1810,7 @@ Instance methods:
17971810
Added the ``fold`` argument.
17981811

17991812

1800-
.. method:: time.isoformat(timespec='auto')
1813+
.. method:: time.isoformat(timespec='auto', use_utc_designator=False)
18011814

18021815
Return a string representing the time in ISO 8601 format, one of:
18031816

@@ -1826,20 +1839,32 @@ Instance methods:
18261839

18271840
:exc:`ValueError` will be raised on an invalid *timespec* argument.
18281841

1842+
If the optional argument *use_utc_designator* is set to :const:`True` and
1843+
:meth:`tzname` returns exactly ``"UTC"``, then "Z" will be given as the UTC
1844+
offset in the formatted string.
1845+
18291846
Example::
18301847

1831-
>>> from datetime import time
1848+
>>> from datetime import time, timezone
18321849
>>> time(hour=12, minute=34, second=56, microsecond=123456).isoformat(timespec='minutes')
18331850
'12:34'
18341851
>>> dt = time(hour=12, minute=34, second=56, microsecond=0)
18351852
>>> dt.isoformat(timespec='microseconds')
18361853
'12:34:56.000000'
18371854
>>> dt.isoformat(timespec='auto')
18381855
'12:34:56'
1856+
>>> dt = time(12, 30, 59, tzinfo=timezone.utc)
1857+
>>> dt.isoformat()
1858+
'12:30:59+00:00'
1859+
>>> dt.isoformat(use_utc_designator=True)
1860+
'12:30:59Z'
18391861

18401862
.. versionadded:: 3.6
18411863
Added the *timespec* argument.
18421864

1865+
.. versionadded:: 3.11
1866+
Added the *use_utc_designator* argument.
1867+
18431868

18441869
.. method:: time.__str__()
18451870

‎Lib/datetime.py‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,21 +1414,26 @@ def __repr__(self):
14141414
s = s[:-1] + ", fold=1)"
14151415
return s
14161416

1417-
def isoformat(self, timespec='auto'):
1417+
def isoformat(self, timespec='auto', use_utc_designator=False):
14181418
"""Return the time formatted according to ISO.
14191419
14201420
The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional
1421-
part is omitted if self.microsecond == 0.
1421+
part is omitted if self.microsecond == 0. The UTC offset will be
1422+
replaced with a single "Z" if use_utc_designator is True and
1423+
self.tzname() is exactly "UTC"
14221424
14231425
The optional argument timespec specifies the number of additional
14241426
terms of the time to include. Valid options are 'auto', 'hours',
14251427
'minutes', 'seconds', 'milliseconds' and 'microseconds'.
14261428
"""
14271429
s = _format_time(self._hour, self._minute, self._second,
14281430
self._microsecond, timespec)
1429-
tz = self._tzstr()
1430-
if tz:
1431-
s += tz
1431+
if use_utc_designator and "UTC" == self.tzname():
1432+
s += "Z"
1433+
else:
1434+
tz = self._tzstr()
1435+
if tz:
1436+
s += tz
14321437
return s
14331438

14341439
__str__ = isoformat
@@ -1894,14 +1899,16 @@ def ctime(self):
18941899
self._hour, self._minute, self._second,
18951900
self._year)
18961901

1897-
def isoformat(self, sep='T', timespec='auto'):
1902+
def isoformat(self, sep='T', timespec='auto', use_utc_designator=False):
18981903
"""Return the time formatted according to ISO.
18991904
19001905
The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'.
19011906
By default, the fractional part is omitted if self.microsecond == 0.
19021907
1903-
If self.tzinfo is not None, the UTC offset is also attached, giving
1904-
giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'.
1908+
If self.tzinfo is not None, the UTC offset is also attached. If the
1909+
optional argument use_utc_designator is True and the timezone name is
1910+
"UTC", a Z is appended: 'YYYY-MM-DD HH:MM:SS.mmmmmmZ'.
1911+
Otherwise the format is 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'.
19051912
19061913
Optional argument sep specifies the separator between date and
19071914
time, default 'T'.
@@ -1915,9 +1922,12 @@ def isoformat(self, sep='T', timespec='auto'):
19151922
self._microsecond, timespec))
19161923

19171924
off = self.utcoffset()
1918-
tz = _format_offset(off)
1919-
if tz:
1920-
s += tz
1925+
if use_utc_designator and "UTC" == self.tzname():
1926+
s += "Z"
1927+
else:
1928+
tz = _format_offset(off)
1929+
if tz:
1930+
s += tz
19211931

19221932
return s
19231933

‎Lib/test/datetimetester.py‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,6 +2010,8 @@ def test_isoformat(self):
20102010
self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05:01.000123")
20112011
self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01.000123")
20122012
self.assertEqual(t.isoformat(sep=' ', timespec='minutes'), "0001-02-03 04:05")
2013+
self.assertEqual(t.isoformat(use_utc_designator=False), "0001-02-03T04:05:01.000123")
2014+
self.assertEqual(t.isoformat(use_utc_designator=True), "0001-02-03T04:05:01.000123")
20132015
self.assertRaises(ValueError, t.isoformat, timespec='foo')
20142016
# bpo-34482: Check that surrogates are handled properly.
20152017
self.assertRaises(ValueError, t.isoformat, timespec='\ud800')
@@ -2018,6 +2020,8 @@ def test_isoformat(self):
20182020

20192021
t = self.theclass(1, 2, 3, 4, 5, 1, 999500, tzinfo=timezone.utc)
20202022
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999+00:00")
2023+
self.assertEqual(t.isoformat(use_utc_designator=False), "0001-02-03T04:05:01.999500+00:00")
2024+
self.assertEqual(t.isoformat(use_utc_designator=True), "0001-02-03T04:05:01.999500Z")
20212025

20222026
t = self.theclass(1, 2, 3, 4, 5, 1, 999500)
20232027
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999")
@@ -2037,6 +2041,8 @@ def test_isoformat(self):
20372041
tz = FixedOffset(timedelta(seconds=16), 'XXX')
20382042
t = self.theclass(2, 3, 2, tzinfo=tz)
20392043
self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16")
2044+
self.assertEqual(t.isoformat(use_utc_designator=False), "0002-03-02T00:00:00+00:00:16")
2045+
self.assertEqual(t.isoformat(use_utc_designator=True), "0002-03-02T00:00:00+00:00:16")
20402046

20412047
def test_isoformat_timezone(self):
20422048
tzoffsets = [
@@ -3276,6 +3282,74 @@ def test_isoformat_timezone(self):
32763282
with self.subTest(tzi=tzi):
32773283
assert t.isoformat() == exp
32783284

3285+
def test_isoformat_utc_designator(self):
3286+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456)
3287+
self.assertEqual(t.isoformat(), "12:34:56.123456")
3288+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456")
3289+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456")
3290+
3291+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3292+
tzinfo=timezone.utc)
3293+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3294+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3295+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z")
3296+
3297+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3298+
tzinfo=timezone(timedelta(0)))
3299+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3300+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3301+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z")
3302+
3303+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3304+
tzinfo=timezone(timedelta(0), "UTC"))
3305+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3306+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3307+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z")
3308+
3309+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3310+
tzinfo=timezone(timedelta(0), "GMT"))
3311+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3312+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3313+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00")
3314+
3315+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3316+
tzinfo=timezone(timedelta(hours=5), "UTC"))
3317+
self.assertEqual(t.isoformat(), "12:34:56.123456+05:00")
3318+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+05:00")
3319+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z")
3320+
3321+
class UnnamedTimezone(tzinfo):
3322+
def utcoffset(self, dt):
3323+
return timedelta(0)
3324+
3325+
def dst(self, dt):
3326+
return timedelta(0)
3327+
3328+
def tzname(self, dt):
3329+
return None
3330+
3331+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3332+
tzinfo=UnnamedTimezone())
3333+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3334+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3335+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00")
3336+
3337+
class NonStringNamedTimezone(tzinfo):
3338+
def utcoffset(self, dt):
3339+
return timedelta(0)
3340+
3341+
def dst(self, dt):
3342+
return timedelta(0)
3343+
3344+
def tzname(self, dt):
3345+
return 42
3346+
3347+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3348+
tzinfo=UnnamedTimezone())
3349+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3350+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3351+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00")
3352+
32793353
def test_1653736(self):
32803354
# verify it doesn't accept extra keyword arguments
32813355
t = self.theclass(second=1)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add *use_utc_designator* as an optional parameter to
2+
:meth:`datetime.datetime.isoformat` and :meth:`datetime.time.isoformat`. If
3+
it's set to true, the UTC offset will be formatted as "Z" rather than "+00:00"
4+
if the object is associated with a timezone named exactly ``"UTC"``.

‎Modules/_datetimemodule.c‎

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,8 +1357,10 @@ format_ctime(PyDateTime_Date *date, int hours, int minutes, int seconds)
13571357
static PyObject *delta_negative(PyDateTime_Delta *self);
13581358

13591359
/* Add formatted UTC offset string to buf. buf has no more than
1360-
* buflen bytes remaining. The UTC offset is gotten by calling
1361-
* tzinfo.uctoffset(tzinfoarg). If that returns None, \0 is stored into
1360+
* buflen bytes remaining. If use_utc_designator is true,
1361+
* tzinfo.tzname(tzinfoarg) will be called, and if it returns "UTC",
1362+
* only "Z\0" will be added. Otherwise, the UTC offset is gotten by calling
1363+
* tzinfo.utcoffset(tzinfoarg). If that returns None, \0 is stored into
13621364
* *buf, and that's all. Else the returned value is checked for sanity (an
13631365
* integer in range), and if that's OK it's converted to an hours & minutes
13641366
* string of the form
@@ -1368,6 +1370,7 @@ static PyObject *delta_negative(PyDateTime_Delta *self);
13681370
*/
13691371
static int
13701372
format_utcoffset(char *buf, size_t buflen, const char *sep,
1373+
int use_utc_designator,
13711374
PyObject *tzinfo, PyObject *tzinfoarg)
13721375
{
13731376
PyObject *offset;
@@ -1376,6 +1379,20 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
13761379

13771380
assert(buflen >= 1);
13781381

1382+
if (use_utc_designator) {
1383+
PyObject* name = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg);
1384+
if (name == NULL)
1385+
return -1;
1386+
int tz_is_utc = (PyUnicode_Check(name) &&
1387+
0 == strcmp("UTC", PyUnicode_AsUTF8(name)));
1388+
Py_DECREF(name);
1389+
1390+
if (tz_is_utc) {
1391+
PyOS_snprintf(buf, buflen, "Z");
1392+
return 0;
1393+
}
1394+
}
1395+
13791396
offset = call_utcoffset(tzinfo, tzinfoarg);
13801397
if (offset == NULL)
13811398
return -1;
@@ -1551,6 +1568,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
15511568
if (format_utcoffset(buf,
15521569
sizeof(buf),
15531570
"",
1571+
0,
15541572
tzinfo,
15551573
tzinfoarg) < 0)
15561574
goto Done;
@@ -4314,7 +4332,8 @@ time_isoformat(PyDateTime_Time *self, PyObject *args, PyObject *kw)
43144332
{
43154333
char buf[100];
43164334
const char *timespec = NULL;
4317-
static char *keywords[] = {"timespec", NULL};
4335+
int use_utc_designator = 0;
4336+
static char *keywords[] = {"timespec", "use_utc_designator", NULL};
43184337
PyObject *result;
43194338
int us = TIME_GET_MICROSECOND(self);
43204339
static const char *specs[][2] = {
@@ -4326,7 +4345,8 @@ time_isoformat(PyDateTime_Time *self, PyObject *args, PyObject *kw)
43264345
};
43274346
size_t given_spec;
43284347

4329-
if (!PyArg_ParseTupleAndKeywords(args, kw, "|s:isoformat", keywords, &timespec))
4348+
if (!PyArg_ParseTupleAndKeywords(args, kw, "|sp:isoformat", keywords,
4349+
&timespec, &use_utc_designator))
43304350
return NULL;
43314351

43324352
if (timespec == NULL || strcmp(timespec, "auto") == 0) {
@@ -4365,8 +4385,8 @@ time_isoformat(PyDateTime_Time *self, PyObject *args, PyObject *kw)
43654385
return result;
43664386

43674387
/* We need to append the UTC offset. */
4368-
if (format_utcoffset(buf, sizeof(buf), ":", self->tzinfo,
4369-
Py_None) < 0) {
4388+
if (format_utcoffset(buf, sizeof(buf), ":", use_utc_designator,
4389+
self->tzinfo, Py_None) < 0) {
43704390
Py_DECREF(result);
43714391
return NULL;
43724392
}
@@ -5548,7 +5568,8 @@ datetime_isoformat(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
55485568
{
55495569
int sep = 'T';
55505570
char *timespec = NULL;
5551-
static char *keywords[] = {"sep", "timespec", NULL};
5571+
int use_utc_designator = 0;
5572+
static char *keywords[] = {"sep", "timespec", "use_utc_designator", NULL};
55525573
char buffer[100];
55535574
PyObject *result = NULL;
55545575
int us = DATE_GET_MICROSECOND(self);
@@ -5561,7 +5582,8 @@ datetime_isoformat(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
55615582
};
55625583
size_t given_spec;
55635584

5564-
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Cs:isoformat", keywords, &sep, &timespec))
5585+
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Csp:isoformat", keywords,
5586+
&sep, &timespec, &use_utc_designator))
55655587
return NULL;
55665588

55675589
if (timespec == NULL || strcmp(timespec, "auto") == 0) {
@@ -5601,8 +5623,8 @@ datetime_isoformat(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
56015623
return result;
56025624

56035625
/* We need to append the UTC offset. */
5604-
if (format_utcoffset(buffer, sizeof(buffer), ":", self->tzinfo,
5605-
(PyObject *)self) < 0) {
5626+
if (format_utcoffset(buffer, sizeof(buffer), ":", use_utc_designator,
5627+
self->tzinfo, (PyObject *)self) < 0) {
56065628
Py_DECREF(result);
56075629
return NULL;
56085630
}

0 commit comments

Comments
 (0)