14

I'm trying to serialize datetime in an API, but I don't want milliseconds. What I want is here: https://en.wikipedia.org/wiki/ISO_8601 - "2015-09-14T17:51:31+00:00"

tz = pytz.timezone('Asia/Taipei')
dt = datetime.datetime.now()
loc_dt = tz.localize(dt)

Try A:

loc_dt.isoformat()
>> '2015-09-17T10:46:15.767000+08:00'

Try B:

loc_dt.strftime("%Y-%m-%dT%H:%M:%S%z")
>> '2015-09-17T10:46:15+0800'

The latter one is almost perfect except it's missing the colon in the timezone part. How can I solve this without string manipulation (deleting milliseconds or adding colon)?

Csaba Toth
  • 10,021
  • 5
  • 75
  • 121
  • I'd prefer a format string solution, because that's what I can easily plop in to the serializer. I don't see anything helpful on the strftime format string page though. – Csaba Toth Sep 17 '15 at 18:05
  • ISO8601 allows second fragments. Try removing the fragment before formatting the string – Panagiotis Kanavos Sep 18 '15 at 07:39
  • unrelated: you code may fail during DST transitions, use `loc_dt = datetime.now(tz)` instead. – jfs Sep 18 '15 at 19:05
  • @J.F.Sebastian What happens, how does it fail? Sometimes what I localize is not datetime.now(), but an existing datetime object. – Csaba Toth Sep 18 '15 at 22:06
  • @CsabaToth: it may return a wrong time for ambiguous local times. If you want to disambiguate existing local times then you need additional info e.g., [Parsing of Ordered Timestamps in Local Time (to UTC) While Observing Daylight Saving Time](http://stackoverflow.com/q/26217427/4279) – jfs Sep 18 '15 at 22:12

2 Answers2

18

You can replace the microseconds with 0 and use isoformat:

import pytz
from datetime import datetime
tz = pytz.timezone('Asia/Taipei')
dt = datetime.now()
loc_dt = tz.localize(dt).replace(microsecond=0)
print loc_dt.isoformat()
2015-09-17T19:12:33+08:00

If you want to keep loc_dt as is do the replacing when you output:

loc_dt = tz.localize(dt)
print loc_dt.replace(microsecond=0).isoformat()

As commented you would be better passing the tz to datetime.now:

 dt = datetime.now(tz)

The reasons are discussed in pep-0495, you might also want to add an assert to catch any bugs when doing the replace:

 ssert loc_dt.resolution >= timedelta(microsecond=0)
Padraic Cunningham
  • 176,452
  • 29
  • 245
  • 321
  • 3
    Wow, that's weird from a parser point of view. If the microsecond is 0 for whatever reason, it is omitted completely form the output? If the minute or second is zero, it's not omitted. – Csaba Toth Sep 17 '15 at 22:28
  • yes, if you replace the milliseconds with 0 then your datetime would becme `datetime.datetime(2015, 9, 17, 12, 33, 46, tzinfo=)` so calling isoformat just works with what is there – Padraic Cunningham Sep 17 '15 at 22:39
  • 2
    Parsing: https://stackoverflow.com/questions/127803/how-to-parse-an-iso-8601-formatted-date-in-python Unbelievable!!! – Csaba Toth Sep 18 '15 at 17:51
  • `tz.localize(datetime.now())` may fail; use `datetime.now(tz)` instead. – jfs Sep 18 '15 at 19:06
  • add `assert datetime.resolution == timedelta(microseconds=1)` otherwise your code may fail. – jfs Sep 18 '15 at 19:15
  • @J.F.Sebastian, what does `datetime.resolution == timedelta(microseconds=1)` do? – Padraic Cunningham Sep 18 '15 at 20:04
  • @CsabaToth, dateutil seems to do a lot of what datetime should be doing but dateutil also involves some guesswork,not sure there is any completely reliable lib out there. – Padraic Cunningham Sep 18 '15 at 20:31
  • @PadraicCunningham: it checks that [`datetime.resolution`](https://docs.python.org/3/library/datetime.html#datetime.datetime.resolution) is 1 microsecond. It is always true for all current Python implementations. Actually, `assert loc_dt.resolution >= timedelta(microsecond=0)` could be better here: the intent is to avoid failing silently if e.g., `loc_dt.resolution` is a nanosecond. Note: failing `assert` here indicates that there is a bug i.e., no user input (even invalid) may make the assert false. – jfs Sep 18 '15 at 21:34
  • @J.F.Sebastian How can the `.replace(microsecond=0).isoformat()` fail? I definitely don't want to tell explicitly to the framework to have microseconds resolution. Another: could you elaborate on how/why `tz.localize(datetime.now())` can fail? – Csaba Toth Sep 18 '15 at 21:40
  • @CsabaToth: (1) imagine you wanted to truncate the result upto a minute: `'2015-09-17T10:46:00+0800'` and you'd used `.replace(second=0).isoformat()` for that; the result would be wrong: `'2015-09-17T10:46:00.767000+08:00` (notice: milliseconds are there). If `loc_dt.resolution` is a nanosecond then `.replace(microsecond=0)` may leave non-zero nanoseconds. (2) on `tz.localize(datetime.now())` -- consider cases then the local time is ambiguous e.g., during "Fall back" DST transition. Read about why `tz.localize()` has `is_dst` parameter. [`datetime.now(tz)` works.](http://goo.gl/xrDWoF) – jfs Sep 18 '15 at 21:52
  • @J.F.Sebastian I see: during DST transition? OMG! – Csaba Toth Sep 18 '15 at 22:05
  • Good solution for parsing for all who use Django: https://stackoverflow.com/questions/20194496/iso-to-datetime-object-z-is-a-bad-directive/27829491#comment53169737_27829491 – Csaba Toth Sep 18 '15 at 22:06
  • @J.F.Sebastian, how does `.replace(second=0).isoformat()` fit into removing the milliseconds? – Padraic Cunningham Sep 19 '15 at 10:27
  • @PadraicCunningham: it doesn't. Notice the words "imagine", "minute" and the corresponding timestamp where seconds are "00". – jfs Sep 19 '15 at 14:57
  • @J.F.Sebastian, so how can ` loc_dt.replace(microsecond=0).isoformat()` fail using the code above, under what circumstances can `loc_dt.resolution` be a nanosecond and result in *non-zero nanoseconds*? – Padraic Cunningham Sep 19 '15 at 15:08
  • @PadraicCunningham: (1) `assert` suggests that it never happens unless there is a bug (2) The bug being that `loc_dt` is some `datetime` subclass such as `pandas.Timestamp` or some other similar type that *might* have `loc_dt.resolution == nanosecond` (there are interfaces with nanonsecond resolution and people want to round-trip without loosing info or convenience) (3) Do I need to explain why `.replace(microsecond=0)` might not be enough if `loc_dt.resolution < microsecond`? – jfs Sep 19 '15 at 15:23
  • @J.F.Sebastian, I understand the why it would fail, I was asking if there were a case directly related to datetime that it could ever fail to which I presume the simple answer is no? – Padraic Cunningham Sep 19 '15 at 15:34
1

Since python 3.6, datetime.isoformat accepts a timespec keyword to pick a precision. This argument gives the smallest time unit you want to be included in the output:

>>> loc_dt.isoformat()
'2022-10-21T19:59:59.991999+08:00'

>>> loc_dt.isoformat(timespec='seconds')
'2022-10-21T19:59:59+08:00'

>>> loc_dt.isoformat(timespec='milliseconds')
'2022-10-21T19:59:59.991+08:00'

Notice how the time is truncated and not rounded.

You can also use timespec to remove seconds/minutes:

>>> loc_dt.isoformat(timespec='minutes')
'2022-10-21T19:59+08:00'

>>> loc_dt.isoformat(timespec='hours')
'2022-10-21T19+08:00'

This all assume you ran the following setup script beforehand:

from datetime import datetime
import pytz

tz = pytz.timezone('Asia/Taipei')
dt = datetime.now()
loc_dt = tz.localize(dt)

Also note that this works without timezone:

>>> from datetime import datetime
>>> now = datetime.now()
>>> now.isoformat(timespec='minutes')
>>> '2022-10-21T19:59'
cglacet
  • 8,873
  • 4
  • 45
  • 60