Reputation: 2482
I know this has been asked but there was no solution provided there. Python UTC datetime object's ISO format doesn't include Z (Zulu or Zero offset)
I am looking for a clean way of generating UTC time stamp with this format in Python. The format I need is
2013-10-29T09:38:41.341Z
.
Specifically, I need to include "Z" at the end. Python's datetime.utcnow().isoformat()
does not append "Z" at the end.
Note that manually appending "Z" is not a solution I can accept. I am looking for a clean way to do this.
What is the clean way to generate UTC timestamp in ISO format with the suffix Z
?
Upvotes: 9
Views: 20353
Reputation: 712
This is how DJango does it [1], [2]:
DjangoJSONEncoder
class django.core.serializers.json.DjangoJSONEncoder¶
The JSON serializer uses DjangoJSONEncoder for encoding. A subclass of JSONEncoder, it handles these additional types:
datetime A string of the form YYYY-MM-DDTHH:mm:ss.sssZ or YYYY-MM-DDTHH:mm:ss.sss+HH:MM as defined in ECMA-262.
def default(self, o):
# See "Date Time String Format" in the ECMA-262 specification.
if isinstance(o, datetime.datetime):
r = o.isoformat()
if o.microsecond:
r = r[:23] + r[26:]
if r.endswith('+00:00'):
r = r[:-6] + 'Z'
return r
print(f"aaa{naive_utcnow.isoformat()[:23] = }")
Please note date datetime
objects may or may not contain timezone information (a distinction called naive and aware datetime objects).
In your example, datetime.utcnow()
will produce a naive object, which will not work properly with django code.
In case you want to always have a Z
in the end (e.g. for interoperability with other systems, such as client browsers and node), take a look at the script below where I explain how to get there as well as how to handle some common pitfalls in handling datetimes with python:
from datetime import datetime, timezone
utc = timezone.utc
naive_utcnow = datetime.utcnow()
aware_utcnow = datetime.now(utc)
# there is no timezone info for naive objects here:
print(f"{naive_utcnow.isoformat() = }")
# with "+00:00":
print(f"{aware_utcnow.isoformat() = }")
# copy & paste from django implementation:
def toECMA262_django(dt: datetime):
s = dt.isoformat()
if dt.microsecond:
s = s[:23] + s[26:]
if s.endswith('+00:00'):
s = s[:-6] + 'Z'
return s
# note: django's version won't add Z for naive objects:
print(f"{toECMA262_django(naive_utcnow) = }")
# djanto's output is perfecly compatible with javacript
# for aware datetime objects:
print(f"{toECMA262_django(aware_utcnow) = }")
# improved version to treat naive objects as utc by default
def toECMA262_v2(dt: datetime, default_tz=utc):
if not dt.tzinfo:
dt = dt.replace(tzinfo=default_tz)
s = dt.isoformat()
if dt.microsecond:
s = s[:23] + s[26:]
if s.endswith('+00:00'):
s = s[:-6] + 'Z'
return s
# now has Z too:
print(f"{toECMA262_v2(naive_utcnow) = }")
print(f"{toECMA262_v2(aware_utcnow) = }")
# now works even with the misleading utcnow():
print(f"{toECMA262_v2(datetime.utcnow()) = }")
# CAREFUL: wrong result here, there is no distinction between
# naive objects returned from now() and utcnow(), the calling
# code is responsible for knowing if naive objects are in utc or not.
print(f"{toECMA262_v2(datetime.now()) = }")
# safer version, no default assumptions made
def toECMA262_v3(dt: datetime, naive_as_tz=None):
if not dt.tzinfo and naive_as_tz:
dt = dt.replace(tzinfo=naive_as_tz)
s = dt.isoformat()
if dt.microsecond:
s = s[:23] + s[26:]
if s.endswith('+00:00'):
s = s[:-6] + 'Z'
return s
# no tz offset for naive objects, unless explicitly specified:
print(f"{toECMA262_v3(naive_utcnow) = }")
print(f"{toECMA262_v3(naive_utcnow, utc) = }")
print(f"{toECMA262_v3(aware_utcnow) = }")
# no tz offset for naive objects, unless explicitly specified:
print(f"{toECMA262_v3(datetime.utcnow()) = }")
print(f"{toECMA262_v3(datetime.utcnow(), utc) = }")
# this is not wrong anymore, but no tz offset either
print(f"{toECMA262_v3(datetime.now()) = }")
# even safer, guarantees there will be a timezone or an exception is raised
def toECMA262_v4(dt: datetime, naive_as_tz=None):
if not dt.tzinfo:
if not naive_as_tz:
raise ValueError('Aware object or naive_as_tz required')
dt = dt.replace(tzinfo=naive_as_tz)
s = dt.isoformat()
if dt.microsecond:
s = s[:23] + s[26:]
if s.endswith('+00:00'):
s = s[:-6] + 'Z'
return s
def try_print(expr):
'''little helper function to print exceptions in place'''
try:
print(f"{expr} = ", end='')
print(repr(eval(expr)))
except ValueError as exc:
print(repr(exc))
# works with naive when tz is explicitly passed, otherwise raise:
try_print("toECMA262_v4(naive_utcnow, utc)")
try_print("toECMA262_v4(naive_utcnow)") # raises
try_print("toECMA262_v4(aware_utcnow)")
try_print("toECMA262_v4(datetime.utcnow(), utc)")
try_print("toECMA262_v4(datetime.utcnow())") # raises
try_print("toECMA262_v4(datetime.now())") # raises
# Please note that if have an aware object that is not in utc,
# you will not get a string ending in Z, but the proper offset
# For example:
import dateutil.tz
tzlocal = dateutil.tz.tzlocal()
aware_now = datetime.now(tzlocal)
print(f"{toECMA262_v4(aware_now) = }")
# output '2021-05-25T04:15:44.848-03:00'
# version that always output Z ended strings:
def toECMA262_v5(dt: datetime, naive_as_tz=None):
if not dt.tzinfo:
if not naive_as_tz:
raise ValueError('Aware object or naive_as_tz required')
dt = dt.replace(tzinfo=naive_as_tz)
dt = dt.astimezone(utc)
s = dt.isoformat()
if dt.microsecond:
s = s[:23] + s[26:]
if s.endswith('+00:00'):
s = s[:-6] + 'Z'
return s
# all possible cases supported and correct now, all returned with Z:
try_print("toECMA262_v5(naive_utcnow, utc)")
try_print("toECMA262_v5(naive_utcnow)") # raises
try_print("toECMA262_v5(aware_utcnow)")
try_print("toECMA262_v5(aware_now)")
try_print("toECMA262_v5(datetime.utcnow(), utc)")
try_print("toECMA262_v5(datetime.utcnow())") # raises
try_print("toECMA262_v5(datetime.now())") # raises
try_print("toECMA262_v5(datetime.now(), tzlocal)") # works fine now ;)
The output of the script:
naive_utcnow.isoformat() = '2021-05-25T07:45:22.774853'
aware_utcnow.isoformat() = '2021-05-25T07:45:22.774856+00:00'
toECMA262_django(naive_utcnow) = '2021-05-25T07:45:22.774'
toECMA262_django(aware_utcnow) = '2021-05-25T07:45:22.774Z'
toECMA262_v2(naive_utcnow) = '2021-05-25T07:45:22.774Z'
toECMA262_v2(aware_utcnow) = '2021-05-25T07:45:22.774Z'
toECMA262_v2(datetime.utcnow()) = '2021-05-25T07:45:22.774Z'
toECMA262_v2(datetime.now()) = '2021-05-25T04:45:22.774Z'
toECMA262_v3(naive_utcnow) = '2021-05-25T07:45:22.774'
toECMA262_v3(naive_utcnow, utc) = '2021-05-25T07:45:22.774Z'
toECMA262_v3(aware_utcnow) = '2021-05-25T07:45:22.774Z'
toECMA262_v3(datetime.utcnow()) = '2021-05-25T07:45:22.775'
toECMA262_v3(datetime.utcnow(), utc) = '2021-05-25T07:45:22.775Z'
toECMA262_v3(datetime.now()) = '2021-05-25T04:45:22.775'
toECMA262_v4(naive_utcnow, utc) = '2021-05-25T07:45:22.774Z'
toECMA262_v4(naive_utcnow) = ValueError('Aware object or naive_as_tz required')
toECMA262_v4(aware_utcnow) = '2021-05-25T07:45:22.774Z'
toECMA262_v4(datetime.utcnow(), utc) = '2021-05-25T07:45:22.775Z'
toECMA262_v4(datetime.utcnow()) = ValueError('Aware object or naive_as_tz required')
toECMA262_v4(datetime.now()) = ValueError('Aware object or naive_as_tz required')
toECMA262_v4(aware_now) = '2021-05-25T04:45:22.788-03:00'
toECMA262_v5(naive_utcnow, utc) = '2021-05-25T07:45:22.774Z'
toECMA262_v5(naive_utcnow) = ValueError('Aware object or naive_as_tz required')
toECMA262_v5(aware_utcnow) = '2021-05-25T07:45:22.774Z'
toECMA262_v5(aware_now) = '2021-05-25T07:45:22.788Z'
toECMA262_v5(datetime.utcnow(), utc) = '2021-05-25T07:45:22.788Z'
toECMA262_v5(datetime.utcnow()) = ValueError('Aware object or naive_as_tz required')
toECMA262_v5(datetime.now()) = ValueError('Aware object or naive_as_tz required')
toECMA262_v5(datetime.now(), tzlocal) = '2021-05-25T07:45:22.788Z'
Version 5 above always output Z
ended ECMA-262 compatible strings, accepting datetime objects in any timezone. If naive datetimes are passed, the caller code must specify if the object is in utc, local or any other timezone and it will be converted to utc automatically.
PS: I used python >= 3.8's newers fstring debug syntax with =
for printing the output in a more friendly/conscise way, besides that the code should run fine with python >= 3.2
Upvotes: 3
Reputation: 71
Due to lack of reputation, I add this as new Answer.
Tomasz Swider Solution is a good start, however [:-3]
will cut off seconds if the time provided does not have microseconds:
I will demonstrate this using utcfromtimestamp:
In [10]: datetime.utcfromtimestamp(0.1).isoformat()[:-3] + 'Z'
Out[10]: '1970-01-01T00:00:00.100Z'
In [10]: datetime.utcfromtimestamp(0).isoformat()[:-3] + 'Z'
Out[11]: '1970-01-01T00:00Z'
I think this is a cleaner solution to get an ISO Date with Milliseconds and 'Z' for Zulu time:
datetime.utcnow().isoformat(timespec='milliseconds')+ 'Z'
Again demonstrating using utcfromtimestamp:
In [25]: datetime.utcfromtimestamp(0.1).isoformat(timespec='milliseconds')+ 'Z'
Out[25]: '1970-01-01T00:00:00.100Z'
In [25]: datetime.utcfromtimestamp(0).isoformat(timespec='milliseconds')+ 'Z'
Out[25]: '1970-01-01T00:00:00.000Z'
Upvotes: 5
Reputation: 47
zulu = "{}Z".format(arrow.utcnow().format('YYYY-MM-DDTHH:mm:ss.SSS'))
#'2018-11-28T21:54:49.639Z'
Upvotes: -1
Reputation: 5474
You can use the arrow
library.
Arrow
doesn't cover it yet, see github issue. And I don't think any python library does it yet. But It is pretty simple to hack for now on.
Need to be installed with pip
though:
$ pip install arrow
Then get your iso format, but without Zulu format
import arrow
arrow.utcnow().isoformat()
#'2017-02-10T08:44:38.954159+00:00'
Or you make your own.
arrow.utcnow().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z'
# 2017-02-11T12:34:30.483Z
Upvotes: 4
Reputation: 2372
How about something like
datetime.utcnow().isoformat()[:-3] + 'Z'
Upvotes: 11