Python UTC datetime object's ISO format doesn't include Z (Zulu or Zero offset)
Asked Answered
D

13

290

Why python 2.7 doesn't include Z character (Zulu or zero offset) at the end of UTC datetime object's isoformat string unlike JavaScript?

>>> datetime.datetime.utcnow().isoformat()
'2013-10-29T09:14:03.895210'

Whereas in javascript

>>> console.log(new Date().toISOString()); 
2013-10-29T09:38:41.341Z
Drinker answered 29/10, 2013 at 9:42 Comment(7)
Python datetime values have NO timezone information. Try pytz or Babel if you want timezone info stored in your time stamp.Cupping
datetime.datetime.utcnow().isoformat() + 'Z'Archilochus
Related: How to get an isoformat datetime string including the default timezone?Archilochus
..and the missing Z surprisingly causes some things not to work eg API callZambrano
It gets even worse, if the last part of datetime is 0, it will truncate it...Phonologist
Best to go with >>> datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") >>>'2021-05-26T07:39:27.909116Z'Sausage
to remove the milliseconds part, use : datetime.utcnow().isoformat(timespec='seconds') + 'Z'Gabriella
C
88

Python datetime objects don't have time zone info by default, and without it, Python actually violates the ISO 8601 specification (if no time zone info is given, assumed to be local time). You can use the pytz package to get some default time zones, or directly subclass tzinfo yourself:

from datetime import datetime, tzinfo, timedelta
class simple_utc(tzinfo):
    def tzname(self,**kwargs):
        return "UTC"
    def utcoffset(self, dt):
        return timedelta(0)

Then you can manually add the time zone info to utcnow():

>>> datetime.utcnow().replace(tzinfo=simple_utc()).isoformat()
'2014-05-16T22:51:53.015001+00:00'

Note that this DOES conform to the ISO 8601 format, which allows for either Z or +00:00 as the suffix for UTC. Note that the latter actually conforms to the standard better, with how time zones are represented in general (UTC is a special case.)

Comely answered 16/5, 2014 at 22:54 Comment(4)
how do I include Z instead of +00:00 ?Paunchy
WARNING!!! pytz violates Python's tzinfo protocol and is dangerous to use! See blog.ganssle.io/articles/2018/03/pytz-fastest-footgun.htmlMeteoritics
@Meteoritics thanks for the link, very interesting. Note that the changes allowing dateutil to work weren't implemented until Python 3.6 with PEP 495, so to say pytz didn't use the standard interface is unfair because the interface changed. Prior to the change the only way to make it work was the way pytz did it.Westleigh
There is zoneinfo first class library now so no need for pytz unless you have ancient environmentsEmission
W
174

Option: isoformat()

Python's datetime does not support the military timezone suffixes like 'Z' suffix for UTC. The following simple string replacement does the trick:

In [1]: import datetime

In [2]: d = datetime.datetime(2014, 12, 10, 12, 0, 0)

In [3]: str(d).replace('+00:00', 'Z')
Out[3]: '2014-12-10 12:00:00Z'

str(d) is essentially the same as d.isoformat(sep=' ')

See: Datetime, Python Standard Library

Option: strftime()

Or you could use strftime to achieve the same effect:

In [4]: d.strftime('%Y-%m-%dT%H:%M:%SZ')
Out[4]: '2014-12-10T12:00:00Z'

Note: This option works only when you know the date specified is in UTC.

See: datetime.strftime()


Additional: Human Readable Timezone

Going further, you may be interested in displaying human readable timezone information, pytz with strftime %Z timezone flag:

In [5]: import pytz

In [6]: d = datetime.datetime(2014, 12, 10, 12, 0, 0, tzinfo=pytz.utc)

In [7]: d
Out[7]: datetime.datetime(2014, 12, 10, 12, 0, tzinfo=<UTC>)

In [8]: d.strftime('%Y-%m-%d %H:%M:%S %Z')
Out[8]: '2014-12-10 12:00:00 UTC'
Walkabout answered 14/3, 2017 at 3:59 Comment(2)
Shouldn't the separator be T instead of a blank?Dric
it should be d = datetime.datetime(2014, 12, 10, 12, 0, 0, tzinfo=datetime.timezone.utc) in line 2. Btw., in RFC3339 it says that a space is 'ok' (instead of the 'T').Hallsy
C
88

Python datetime objects don't have time zone info by default, and without it, Python actually violates the ISO 8601 specification (if no time zone info is given, assumed to be local time). You can use the pytz package to get some default time zones, or directly subclass tzinfo yourself:

from datetime import datetime, tzinfo, timedelta
class simple_utc(tzinfo):
    def tzname(self,**kwargs):
        return "UTC"
    def utcoffset(self, dt):
        return timedelta(0)

Then you can manually add the time zone info to utcnow():

>>> datetime.utcnow().replace(tzinfo=simple_utc()).isoformat()
'2014-05-16T22:51:53.015001+00:00'

Note that this DOES conform to the ISO 8601 format, which allows for either Z or +00:00 as the suffix for UTC. Note that the latter actually conforms to the standard better, with how time zones are represented in general (UTC is a special case.)

Comely answered 16/5, 2014 at 22:54 Comment(4)
how do I include Z instead of +00:00 ?Paunchy
WARNING!!! pytz violates Python's tzinfo protocol and is dangerous to use! See blog.ganssle.io/articles/2018/03/pytz-fastest-footgun.htmlMeteoritics
@Meteoritics thanks for the link, very interesting. Note that the changes allowing dateutil to work weren't implemented until Python 3.6 with PEP 495, so to say pytz didn't use the standard interface is unfair because the interface changed. Prior to the change the only way to make it work was the way pytz did it.Westleigh
There is zoneinfo first class library now so no need for pytz unless you have ancient environmentsEmission
K
62

Short answer

datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")

Long answer

The reason that the "Z" is not included is because datetime.now() and even datetime.utcnow() return timezone naive datetimes, that is to say datetimes with no timezone information associated. To get a timezone aware datetime, you need to pass a timezone as an argument to datetime.now. For example:

from datetime import datetime, timezone

datetime.utcnow()
#> datetime.datetime(2020, 9, 3, 20, 58, 49, 22253)
# This is timezone naive

datetime.now(timezone.utc)
#> datetime.datetime(2020, 9, 3, 20, 58, 49, 22253, tzinfo=datetime.timezone.utc)
# This is timezone aware

Once you have a timezone aware timestamp, isoformat will include a timezone designation. Thus, you can then get an ISO 8601 timestamp via:

datetime.now(timezone.utc).isoformat()
#> '2020-09-03T20:53:07.337670+00:00'

"+00:00" is a valid ISO 8601 timezone designation for UTC. If you want to have "Z" instead of "+00:00", you have to do the replacement yourself:

datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
#> '2020-09-03T20:53:07.337670Z'
Kiersten answered 3/9, 2020 at 21:2 Comment(3)
I like this answer because most of the others either don't demonstrate adding a Z, or use naive datetime objects. I feel like using aware objects is the way to go. Once I saw the difference between datetime.utcnow().timestamp() and datetime.now(timezone.utc).timestamp() I decided to try to stick to aware objects.Gilbertina
Also, if you really want to match the value returned in Javascript, you could do this: datetime.now(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z'). It's a bit lengthy but maybe also easy to read?Gilbertina
I think this is the best answer given the API limitations, but this library really ought to have a method to achieve this. Manually replacing strings components for a very common operation feels brittle and should be unnecessary.Rexanne
N
40

The following javascript and python scripts give identical outputs. I think it's what you are looking for.

JavaScript

new Date().toISOString()

Python

from datetime import datetime

datetime.utcnow().isoformat()[:-3]+'Z'

The output they give is the UTC (zulu) time formatted as an ISO string with a 3 millisecond significant digit and appended with a Z.

2019-01-19T23:20:25.459Z
Norling answered 19/1, 2019 at 23:24 Comment(4)
But if now() is 0 milliseconds, minutes and : will be stripped too.Christianize
@МихаилМуругов when you run .isoformat(), it creates a string with a fixed number of characters including trailing zeros, so this does work reliablyPasquale
@PeterVanderMeer datetime(2021, 2, 19, 23, 21, 15).isoformat() -> '2021-02-19T23:21:15', datetime(2021, 2, 19, 23, 21, 15, 256).isoformat() -> '2021-02-19T23:21:15.000256'. Different representations. My mistake is that I wrote about minutes, not seconds.Christianize
With timezone offset you need to use [:-6]+'Z'.Khmer
D
33

Your goal shouldn't be to add a Z character, it should be to generate a UTC "aware" datetime string in ISO 8601 format. The solution is to pass a UTC timezone object to datetime.now() instead of using datetime.utcnow():

from datetime import datetime, timezone

datetime.now(timezone.utc)
>>> datetime.datetime(2020, 1, 8, 6, 6, 24, 260810, tzinfo=datetime.timezone.utc)

datetime.now(timezone.utc).isoformat()
>>> '2020-01-08T06:07:04.492045+00:00'

That looks good, so let's see what Django and dateutil think:

from django.utils.timezone import is_aware
is_aware(datetime.now(timezone.utc))
>>> True

from dateutil.parser import isoparse
is_aware(isoparse(datetime.now(timezone.utc).isoformat()))
>>> True

Note that you need to use isoparse() from dateutil.parser because the Python documentation for datetime.fromisoformat() says it "does not support parsing arbitrary ISO 8601 strings".

Okay, the Python datetime object and the ISO 8601 string are both UTC "aware". Now let's look at what JavaScript thinks of the datetime string. Borrowing from this answer we get:

let date = '2020-01-08T06:07:04.492045+00:00';
const dateParsed = new Date(Date.parse(date))

document.write(dateParsed);
document.write("\n");
// Tue Jan 07 2020 22:07:04 GMT-0800 (Pacific Standard Time)

document.write(dateParsed.toISOString());
document.write("\n");
// 2020-01-08T06:07:04.492Z

document.write(dateParsed.toUTCString());
document.write("\n");
// Wed, 08 Jan 2020 06:07:04 GMT

Notes:

I approached this problem with a few goals:

  • generate a UTC "aware" datetime string in ISO 8601 format
  • use only Python Standard Library functions for datetime object and string creation
  • validate the datetime object and string with the Django timezone utility function, the dateutil parser and JavaScript functions

Note that this approach does not include a Z suffix and does not use utcnow(). But it's based on the recommendation in the Python documentation and it passes muster with both Django and JavaScript.

See also:

Drawplate answered 8/1, 2020 at 7:17 Comment(2)
Bonus for sticking to base library functions and following up on an older post.Hasen
Bonus for appropriately identifying and addressing an XY Problem. The root issue is TZ "awareness". +00:00 is valid ISO 8601 and works (as demonstrated) machine-to-machine. Using Z is purely a presentation problem. In OP's post, the Python example had no TZ awareness while the JS example did. The fact JS prefers the Z notation is incidental. Other answers are reframing OP's "why" question into "how" (while presupposing Z specifically is the goal).Disillusion
T
22

In Python >= 3.2 you can simply use this:

>>> from datetime import datetime, timezone
>>> datetime.now(timezone.utc).isoformat()
'2019-03-14T07:55:36.979511+00:00'
Trici answered 14/3, 2019 at 7:56 Comment(1)
Very nice solution. For their question about the Z, you can clip the offset: .isoformat()[:-6] + 'Z'Brainwork
M
12

Python datetimes are a little clunky. Use arrow.

> str(arrow.utcnow())
'2014-05-17T01:18:47.944126+00:00'

Arrow has essentially the same api as datetime, but with timezones and some extra niceties that should be in the main library.

A format compatible with Javascript can be achieved by:

arrow.utcnow().isoformat().replace("+00:00", "Z")
'2018-11-30T02:46:40.714281Z'

Javascript Date.parse will quietly drop microseconds from the timestamp.

Mouser answered 17/5, 2014 at 1:19 Comment(1)
This doesn't answer the question as the string doesn't end with 'Z'.Bookworm
T
7

I use pendulum:

import pendulum


d = pendulum.now("UTC").to_iso8601_string()
print(d)

>>> 2019-10-30T00:11:21.818265Z
Tankersley answered 30/10, 2019 at 0:19 Comment(0)
E
5

Using only standard libraries, making no assumption that the timezone is already UTC, and returning the exact format requested in the question:

dt.astimezone(timezone.utc).replace(tzinfo=None).isoformat(timespec='milliseconds') + 'Z'

This does require Python 3.6 or later though.

Evans answered 9/9, 2021 at 10:43 Comment(0)
V
4

There are a lot of good answers on the post, but I wanted the format to come out exactly as it does with JavaScript. This is what I'm using and it works well.

In [1]: import datetime

In [1]: now = datetime.datetime.utcnow()

In [1]: now.strftime('%Y-%m-%dT%H:%M:%S') + now.strftime('.%f')[:4] + 'Z'
Out[3]: '2018-10-16T13:18:34.856Z'
Visional answered 16/10, 2018 at 13:22 Comment(0)
P
2
>>> import arrow

>>> now = arrow.utcnow().format('YYYY-MM-DDTHH:mm:ss.SSS')
>>> now
'2018-11-28T21:34:59.235'
>>> zulu = "{}Z".format(now)
>>> zulu
'2018-11-28T21:34:59.235Z'

Or, to get it in one fell swoop:

>>> arrow.utcnow().format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")
'2018-11-28T21:54:49.639Z'
Pangolin answered 28/11, 2018 at 21:42 Comment(1)
Now you can use escape characters to make it simple. zulu = arrow.utcnow().format("YYYY-MM-DDTHH:mm:ss:SSS[Z]")Hafiz
Y
1

By combining all answers above I came with following function :

from datetime import datetime, tzinfo, timedelta
class simple_utc(tzinfo):
    def tzname(self,**kwargs):
        return "UTC"
    def utcoffset(self, dt):
        return timedelta(0)


def getdata(yy, mm, dd, h, m, s) :
    d = datetime(yy, mm, dd, h, m, s)
    d = d.replace(tzinfo=simple_utc()).isoformat()
    d = str(d).replace('+00:00', 'Z')
    return d


print getdata(2018, 02, 03, 15, 0, 14)
Yolanda answered 28/2, 2018 at 10:24 Comment(0)
W
0
pip install python-dateutil
>>> a = "2019-06-27T02:14:49.443814497Z"
>>> dateutil.parser.parse(a)
datetime.datetime(2019, 6, 27, 2, 14, 49, 443814, tzinfo=tzutc())
Warthman answered 27/6, 2019 at 6:21 Comment(1)
I don't think this answers the question. It shows the opposite, i.e. conversion from string to datetime. However it was asked to convert from datetime to string with a certain format.Hallsy

© 2022 - 2024 — McMap. All rights reserved.