905

I need to parse RFC 3339 strings like "2008-09-03T20:56:35.450686Z" into Python's datetime type.

I have found strptime in the Python standard library, but it is not very convenient.

What is the best way to do this?

Community
  • 1
  • 1
Alexander Artemenko
  • 21,378
  • 8
  • 39
  • 36
  • 1
    related: [Convert timestamps with offset to datetime obj using strptime](http://stackoverflow.com/q/12281975/4279) – jfs Feb 01 '16 at 20:00
  • 11
    To be clear: [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) is the main standard. [RFC 3339](https://tools.ietf.org/html/rfc3339) is a self-proclaimed “profile” of ISO 8601 that makes some [unwise overrides](https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC) of ISO 8601 rules. – Basil Bourque Aug 03 '18 at 23:43

29 Answers29

663

isoparse function from python-dateutil

The python-dateutil package has dateutil.parser.isoparse to parse not only RFC 3339 datetime strings like the one in the question, but also other ISO 8601 date and time strings that don't comply with RFC 3339 (such as ones with no UTC offset, or ones that represent only a date).

>>> import dateutil.parser
>>> dateutil.parser.isoparse('2008-09-03T20:56:35.450686Z') # RFC 3339 format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc())
>>> dateutil.parser.isoparse('2008-09-03T20:56:35.450686') # ISO 8601 extended format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
>>> dateutil.parser.isoparse('20080903T205635.450686') # ISO 8601 basic format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
>>> dateutil.parser.isoparse('20080903') # ISO 8601 basic format, date only
datetime.datetime(2008, 9, 3, 0, 0)

The python-dateutil package also has dateutil.parser.parse. Compared with isoparse, it is presumably less strict, but both of them are quite forgiving and will attempt to interpret the string that you pass in. If you want to eliminate the possibility of any misreads, you need to use something stricter than either of these functions.

Comparison with Python 3.7+’s built-in datetime.datetime.fromisoformat

dateutil.parser.isoparse is a full ISO-8601 format parser, but in Python ≤ 3.10 fromisoformat is deliberately not. In Python 3.11, fromisoformat supports almost all strings in valid ISO 8601. See fromisoformat's docs for this cautionary caveat. (See this answer).

Flimm
  • 136,138
  • 45
  • 251
  • 267
  • 106
    For the lazy, it's installed via `python-dateutil` not `dateutil`, so: `pip install python-dateutil`. – cod3monk3y Mar 12 '14 at 21:55
  • 32
    Be warned that the `dateutil.parser` is intentionally hacky: it tries to guess the format and makes inevitable assumptions (customizable by hand only) in ambiguous cases. So ONLY use it if you need to parse input of unknown format and are okay to tolerate occasional misreads. – ivan_pozdeev Apr 23 '15 at 23:34
  • 2
    Agreed. An example is passing a "date" of 9999. This will return the same as datetime(9999, current month, current day). Not a valid date in my view. – timbo Jun 23 '16 at 23:08
  • 2
    @ivan_pozdeev what package would you recommend for non-guessing parsing? – bgusach Jan 10 '18 at 12:54
  • 1
    @bgusach `iso8601` [as another answer suggests](https://stackoverflow.com/questions/127803/how-to-parse-an-iso-8601-formatted-date/127934#127934). – ivan_pozdeev Jan 10 '18 at 13:06
  • @ivan_pozdeev but that's for `iso8601` not `rfc3339`. Although the question is kind of confusing, seems to treat both as the same. I though we were talking only about the `rfc3339` – bgusach Jan 10 '18 at 13:50
  • @bgusach [RFC 3339](https://tools.ietf.org/html/rfc3339), right in the abstract: _"This document defines a date and time format for use in Internet protocols that is a profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar."_ – ivan_pozdeev Jan 10 '18 at 16:57
  • @ivan_pozdeev I stand corrected then, thanks. I took a look at the doc, but did not understand that `a profile of the ISO 8601` means `a strict subset of ISO 8601` (I'm not a native speaker). BTW, there seems to be an minor incompatibility between the both with the TZ `-00:00`, but I don't think that can cause any trouble in my case. – bgusach Jan 11 '18 at 15:22
  • 1
    In Python 3, the parser always uses the `tzlocal` time zone, regardless of `Z` appearing at the end of the time string, on systems that are configured to use UTC as their default time zone. Numeric offsets produce a `tzoffset` tzinfo object. – Throw Away Account Oct 01 '18 at 23:10
  • For a shorter way to write it down you can do: `from dateutil.parser import parse as parsedate` and then use `parsedate()` instead of `dateutil.parser.parse()` – gitaarik Mar 04 '19 at 17:31
  • 2
    @ivan_pozdeev there's an update to the module that reads iso8601 dates: https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.isoparse – theEpsilon Jan 14 '20 at 17:30
  • It is a pity, you have to install a third party library for a very common use of a date format, i mean the notation ending with Z. – ᐅdevrimbaris Mar 11 '21 at 14:20
  • 1
    This should be fixed in Python 3.11 – noamcohen97 Nov 24 '22 at 09:43
  • @noamcohen97 Thanks. I edited this answer and the other answer for Python 3.11 – Flimm Nov 24 '22 at 09:54
463

Since Python 3.11, the standard library’s datetime.fromisoformat supports any valid ISO 8601 input. In earlier versions it only parses a specific subset, see the cautionary note in the docs. If you are using Python 3.10 or earlier, see other answers for functions from outside the standard library. The docs:

classmethod datetime.fromisoformat(date_string):

Return a datetime corresponding to a date_string in any valid ISO 8601 format, with the following exceptions:

  1. Time zone offsets may have fractional seconds.
  2. The T separator may be replaced by any single unicode character.
  3. Ordinal dates are not currently supported.
  4. Fractional hours and minutes are not supported.

Examples:

>>> from datetime import datetime
>>> datetime.fromisoformat('2011-11-04')
datetime.datetime(2011, 11, 4, 0, 0)
>>> datetime.fromisoformat('20111104')
datetime.datetime(2011, 11, 4, 0, 0)
>>> datetime.fromisoformat('2011-11-04T00:05:23')
datetime.datetime(2011, 11, 4, 0, 5, 23)
>>> datetime.fromisoformat('2011-11-04T00:05:23Z')
datetime.datetime(2011, 11, 4, 0, 5, 23, tzinfo=datetime.timezone.utc)
>>> datetime.fromisoformat('20111104T000523')
datetime.datetime(2011, 11, 4, 0, 5, 23)
>>> datetime.fromisoformat('2011-W01-2T00:05:23.283')
datetime.datetime(2011, 1, 4, 0, 5, 23, 283000)
>>> datetime.fromisoformat('2011-11-04 00:05:23.283')
datetime.datetime(2011, 11, 4, 0, 5, 23, 283000)
>>> datetime.fromisoformat('2011-11-04 00:05:23.283+00:00')
datetime.datetime(2011, 11, 4, 0, 5, 23, 283000, tzinfo=datetime.timezone.utc)
>>> datetime.fromisoformat('2011-11-04T00:05:23+04:00')   
datetime.datetime(2011, 11, 4, 0, 5, 23, tzinfo=datetime.timezone(datetime.timedelta(seconds=14400)))

New in version 3.7.

Changed in version 3.11: Previously, this method only supported formats that could be emitted by date.isoformat() or datetime.isoformat().

Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
Taku
  • 31,927
  • 11
  • 74
  • 85
  • 12
    That's weird. Because a `datetime` may contain a `tzinfo`, and thus output a timezone, but `datetime.fromisoformat()` doesn't parse the tzinfo ? seems like a bug .. – Hendy Irawan Jul 17 '18 at 13:23
  • 66
    Don't miss that note in the documentation, this doesn't accept *all* valid ISO 8601 strings, only ones generated by `isoformat`. It doesn't accept the example in the question `"2008-09-03T20:56:35.450686Z"` because of the trailing `Z`, but it does accept `"2008-09-03T20:56:35.450686"`. – Flimm Aug 23 '18 at 16:27
  • 79
    To properly support the `Z` the input script can be modified with `date_string.replace("Z", "+00:00")`. – jox Dec 02 '18 at 10:47
  • 17
    Note that for seconds it only handles either exactly 0, 3 or 6 decimal places. If the input data has 1, 2, 4, 5, 7 or more decimal places, parsing will fail! – Felk May 28 '19 at 21:30
  • As noted, this method will only successfully parse the output of `isoformat`, and is not fully ISO-8601 compliant, but very few languages are fully compliant given how large and arcane that standard is. Yes Java will accept timezones and date offsets, but anything further than that will fall over as well – user1596707 Aug 04 '20 at 08:38
  • @jox: Do you mean `"+0000"` instead of `"+00:00"`? I am looking at the docs for `datetime.strptime()` and `%z` here: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes – kevinarpe Dec 29 '21 at 04:52
  • 4
    @kevinarpe no, `datetime.fromisoformat` seems to expect another format. I just tested both versions and while it works fine with `+00:00`, I get _"ValueError: Invalid isoformat string"_ with `+0000`. – jox Dec 29 '21 at 15:30
  • 1
    @jox Great feedback. So `datetime.fromisoformat` is even more insane that I thought! How can Python be such a great language and ecosystem, but have such horrible date/time handling? My Python date/time code is usually littered with "gotcha" comments and links to SO.com answers / comments! – kevinarpe Dec 29 '21 at 15:56
  • 2
    `fromisoformat` accepts almost all ISO 8601 date strings in Python 3.11 now, so a lot of these comments are out of date. – Flimm Nov 24 '22 at 09:54
233

Note in Python 2.6+ and Py3K, the %f character catches microseconds.

>>> datetime.datetime.strptime("2008-09-03T20:56:35.450686Z", "%Y-%m-%dT%H:%M:%S.%fZ")

See issue here

nofinator
  • 2,906
  • 21
  • 25
sethbc
  • 3,441
  • 1
  • 16
  • 9
  • 4
    Note - if using Naive datetimes - I think you get no TZ at all - Z may not match anything. – Danny Staple Feb 02 '15 at 17:08
  • 2
    in my case %f caught microseconds rather than Z, `datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f')` so this did the trick – ashim888 Feb 09 '16 at 05:33
  • 2
    Does Py3K mean Python 3000?!? – Robino Nov 13 '17 at 15:35
  • 1
    Fails if no ms or tz. – Robino Nov 13 '17 at 17:02
  • 4
    @Robino IIRC, "Python 3000" is an old name for what is now known as Python 3. – Throw Away Account Oct 01 '18 at 23:14
  • 1
    This answer (in its current, edited form) relies upon hard-coding a particular UTC offset (namely "Z", which means +00:00) into the format string. This is a bad idea because it will fail to parse any datetime with a different UTC offset and raise an exception. Also, even if you use this to parse a datetime with an offset of `Z`, you'll get back a "naive" `datetime` object with no timezone, instead of "timezone-aware" one with UTC as the timezone, which would be more correct. – Mark Amery Oct 09 '22 at 18:56
  • fail for this string: 2008-09-03T20:56:35Z – omriman12 Feb 23 '23 at 12:05
194

As of Python 3.7, you can basically (caveats below) get away with using datetime.datetime.strptime to parse RFC 3339 datetimes, like this:

from datetime import datetime

def parse_rfc3339(datetime_str: str) -> datetime:
    try:
        return datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%S.%f%z")
    except ValueError:
        # Perhaps the datetime has a whole number of seconds with no decimal
        # point. In that case, this will work:
        return datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%S%z")

It's a little awkward, since we need to try two different format strings in order to support both datetimes with a fractional number of seconds (like 2022-01-01T12:12:12.123Z) and those without (like 2022-01-01T12:12:12Z), both of which are valid under RFC 3339. But as long as we do that single fiddly bit of logic, this works.

Some caveats to note about this approach:

  • It technically doesn't fully support RFC 3339, since RFC 3339 bizarrely lets you use a space instead of a T to separate the date from the time, even though RFC 3339 purports to be a profile of ISO 8601 and ISO 8601 does not allow this. If you want to support this silly quirk of RFC 3339, you could add datetime_str = datetime_str.replace(' ', 'T') to the start of the function.
  • My implementation above is slightly more permissive than a strict RFC 3339 parser should be, since it will allow timezone offsets like +0500 without a colon, which RFC 3339 does not support. If you don't merely want to parse known-to-be-RFC-3339 datetimes but also want to rigorously validate that the datetime you're getting is RFC 3339, use another approach or add in your own logic to validate the timezone offset format.
  • This function definitely doesn't support all of ISO 8601, which includes a much wider array of formats than RFC 3339. (e.g. 2009-W01-1 is a valid ISO 8601 date.)
  • It does not work in Python 3.6 or earlier, since in those old versions the %z specifier only matches timezones offsets like +0500 or -0430 or +0000, not RFC 3339 timezone offsets like +05:00 or -04:30 or Z.
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
85

Try the iso8601 module; it does exactly this.

There are several other options mentioned on the WorkingWithTime page on the python.org wiki.

Flimm
  • 136,138
  • 45
  • 251
  • 267
Nicholas Riley
  • 43,532
  • 6
  • 101
  • 124
  • 1
    Simple as `iso8601.parse_date("2008-09-03T20:56:35.450686Z")` – Pakman Apr 25 '12 at 22:36
  • 3
    The question wasn't "how do I parse ISO 8601 dates", it was "how do I parse this exact date format." – Nicholas Riley Sep 20 '12 at 11:04
  • 3
    @tiktak The OP asked "I need to parse strings like X" and my reply to that, having tried both libraries, is to use another one, because iso8601 has important issues still open. My involvement or lack thereof in such a project is completely unrelated to the answer. – Tobia Jan 28 '13 at 08:56
  • 6
    [iso8601](https://pypi.python.org/pypi/iso8601/), a.k.a. *pyiso8601*, has been updated as recently as Feb 2014. The latest version supports a much broader set of ISO 8601 strings. I've been using to good effect in some of my projects. – Dave Hein Nov 13 '14 at 00:50
  • 1
    Sadly that lib called "iso8601" on pypi is trivially incomplete. It clearly states it doesn't handle dates based on week numbers just to pick one example. – boxed Jan 05 '16 at 12:29
  • @Tobia: [iso8601](https://pypi.python.org/pypi/iso8601/0.1.11) seems to be getting updates again. – Georg Schölly Aug 03 '16 at 09:19
63

Python >= 3.11

fromisoformat now parses Z directly:

from datetime import datetime

s = "2008-09-03T20:56:35.450686Z"

datetime.fromisoformat(s)
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=datetime.timezone.utc)

Python 3.7 to 3.10

A simple option from one of the comments: replace 'Z' with '+00:00' - and use fromisoformat:

from datetime import datetime

s = "2008-09-03T20:56:35.450686Z"

datetime.fromisoformat(s.replace('Z', '+00:00'))
# datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=datetime.timezone.utc)

Why prefer fromisoformat?

Although strptime's %z can parse the 'Z' character to UTC, fromisoformat is faster by ~ x40 (or even ~x60 for Python 3.11):

from datetime import datetime
from dateutil import parser

s = "2008-09-03T20:56:35.450686Z"

# Python 3.11+
%timeit datetime.fromisoformat(s)
85.1 ns ± 0.473 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

# Python 3.7 to 3.10
%timeit datetime.fromisoformat(s.replace('Z', '+00:00'))
134 ns ± 0.522 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

%timeit parser.isoparse(s)
4.09 µs ± 5.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

%timeit datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f%z')
5 µs ± 9.26 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

%timeit parser.parse(s)
28.5 µs ± 99.2 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

(Python 3.11.3 x64 on GNU/Linux)

See also: A faster strptime

FObersteiner
  • 22,500
  • 8
  • 42
  • 72
  • 2
    @mikerodent: the point is that `fromisoformat` parses `+00:00` but not `Z` to [aware datetime](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) with tzinfo being UTC. If your input e.g. ends with `Z+00:00`, you can just remove the `Z` before feeding it into `fromisoformat`. Other UTC offsets like e.g. `+05:30` will then be parsed to a static UTC offset (not an actual time zone). – FObersteiner Mar 28 '21 at 10:00
  • Totally different point now (I understand a bit more about "awareness"). But I've noted in the docs "Changed in version 3.11: Previously, this method only supported formats that could be emitted by date.isoformat() or datetime.isoformat()." and "corresponding to a date_string in any valid ISO 8601 format". That might actually not be what one wants. The `fromisoformat` of `datetime.date` is more explicit: "Return a date corresponding to a date_string given in any valid ISO 8601 format... " ... and it gives some surprising examples of strings which work which are not simple YYYY-MM-DD. – mike rodent Jun 15 '23 at 19:54
49

Starting from Python 3.7, strptime supports colon delimiters in UTC offsets (source). So you can then use:

import datetime

def parse_date_string(date_string: str) -> datetime.datetime
    try:
       return datetime.datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%f%z')
    except ValueError:
       return datetime.datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S%z')

EDIT:

As pointed out by Martijn, if you created the datetime object using isoformat(), you can simply use datetime.fromisoformat().

EDIT 2:

As pointed out by Mark Amery, I added a try..except block to account for missing fractional seconds.

Andreas Profous
  • 1,384
  • 13
  • 10
  • 8
    But in 3.7, you *also* have `datetime.fromisoformat()` which handles strings like your input automatically: `datetime.datetime.isoformat('2018-01-31T09:24:31.488670+00:00')`. – Martijn Pieters Jan 30 '19 at 12:53
  • 2
    Good point. I agree, I recommend to use `datetime.fromisoformat()` and `datetime.isoformat()` – Andreas Profous Jun 19 '19 at 20:11
  • This is the only answer that actually meets the question criteria. If you have to use strptime this is the correct answer – Danielo515 Feb 22 '21 at 06:53
  • You example fails on Python 3.6 with: `ValueError: time data '2018-01-31T09:24:31.488670+00:00' does not match format '%Y-%m-%dT%H:%M:%S.%f%z'` that's due to `%z` not matching `+00:00`. However `+0000` matches `%z` see python doc https://docs.python.org/3.6/library/datetime.html#strftime-and-strptime-behavior – Eric Mar 30 '21 at 13:30
  • @Eric Yes, this answer only works in Python 3.7 or newer. – Flimm Jun 01 '21 at 09:13
  • Alas, neither your `strptime` incantation nor `fromisoformat()` as @MartijnPieters suggests are sufficient to parse even all valid RFC 3339 datetimes (let alone ISO 8601, of course). Your `strptime` incantation chokes if given input without a fractional number of seconds (e.g. `'2018-01-31T09:24:31+00:00'`, while `fromisoformat` can't handle a timezone offset of `Z` (like used in the example in the question, or output from JavaScript's `Date.toISOString()` method). – Mark Amery Oct 09 '22 at 18:09
  • @MarkAmery: You're right about the strptime limitation. I adjusted the answer for your edge case, thanks. Regarding fromisoformat: I would only use it if you created the date string with isoformat() also (as stated in the answer). – Andreas Profous Oct 10 '22 at 19:25
  • 3
    @MarkAmery: Python 3.11 has further improved `fromisoformat()` and it can handle the `Z` timezone now: `datetime.fromisoformat('2018-01-31T09:24:31Z')` produces `datetime.datetime(2018, 1, 31, 9, 24, 31, tzinfo=datetime.timezone.utc)`. – Martijn Pieters Nov 25 '22 at 22:29
39

What is the exact error you get? Is it like the following?

>>> datetime.datetime.strptime("2008-08-12T12:20:30.656234Z", "%Y-%m-%dT%H:%M:%S.Z")
ValueError: time data did not match format:  data=2008-08-12T12:20:30.656234Z  fmt=%Y-%m-%dT%H:%M:%S.Z

If yes, you can split your input string on ".", and then add the microseconds to the datetime you got.

Try this:

>>> def gt(dt_str):
        dt, _, us= dt_str.partition(".")
        dt= datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S")
        us= int(us.rstrip("Z"), 10)
        return dt + datetime.timedelta(microseconds=us)

>>> gt("2008-08-12T12:20:30.656234Z")
datetime.datetime(2008, 8, 12, 12, 20, 30, 656234)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
tzot
  • 92,761
  • 29
  • 141
  • 204
  • 12
    You can't just strip .Z because it means timezone and can be different. I need to convert date to the UTC timezone. – Alexander Artemenko Sep 24 '08 at 15:49
  • A plain datetime object has no concept of timezone. If all your times are ending in "Z", all the datetimes you get are UTC (Zulu time). – tzot Sep 24 '08 at 16:03
  • if the timezone is anything other than `""` or `"Z"`, then it must be an offset in hours/minutes, which can be directly added to/subtracted from the datetime object. you *could* create a tzinfo subclass to handle it, but that's probably not reccomended. – SingleNegationElimination Jul 04 '11 at 22:24
  • 9
    Additionally, "%f" is the microsecond specifier, so a (timezone-naive) strptime string looks like: "%Y-%m-%dT%H:%M:%S.%f" . – quodlibetor Jul 16 '12 at 16:52
  • 1
    This will raise an exception if the given datetime string has a UTC offset other than "Z". It does not support the entire RFC 3339 format and is an inferior answer to others that handle UTC offsets properly. – Mark Amery Jun 07 '15 at 18:12
  • Why not use the `%f` I don't get it ? I just saw this post because of it was used as a duplicate on https://stackoverflow.com/questions/69953076/how-do-i-convert-type-of-a-datetime-string-which-has-z-in-it/69953133#69953133 but that seems not easy regarding juts use `"%Y-%m-%dT%H:%M:%S.%fZ"` – azro Nov 13 '21 at 09:45
  • 1
    Python 3.11 has a much improved `datetime.fromisoformat` which will handle most iso8601 and rfc3339 formats. https://docs.python.org/3.11/library/datetime.html#datetime.datetime.fromisoformat – Nelson Aug 09 '22 at 14:20
28
import re
import datetime
s = "2008-09-03T20:56:35.450686Z"
d = datetime.datetime(*map(int, re.split(r'[^\d]', s)[:-1]))
Flimm
  • 136,138
  • 45
  • 251
  • 267
Ted
  • 1,780
  • 1
  • 11
  • 5
  • 89
    I disagree, this is practically unreadable and as far as I can tell does not take into account the Zulu (Z) which makes this datetime naive even though time zone data was provided. – umbrae Dec 21 '11 at 15:02
  • 15
    I find it quite readable. In fact, it's probably the easiest and most performing way to do the conversion without installing additional packages. – Tobia Nov 21 '12 at 14:27
  • 3
    This is equivalent of d=datetime.datetime(*map(int, re.split('\D', s)[:-1])) i suppose. – Xuan May 21 '13 at 09:18
  • def from_utc(date_str): """ Convert UTC time data string to time.struct_time """ UTC_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" return time.strptime(date_str, UTC_FORMAT) – enchanter Mar 27 '14 at 22:41
  • 4
    a variation: `datetime.datetime(*map(int, re.findall('\d+', s))` – jfs May 16 '14 at 02:19
  • 5
    This results in a naive datetime object without timezone, right? So the UTC bit gets lost in translation? – w00t Jun 12 '14 at 21:46
  • 1
    @w00t: `aware_d = d.replace(tzinfo=timezone.utc)` – jfs Oct 25 '14 at 03:24
  • 1
    This has the benefit of working with incomplete iso strings including dates and second-less datetimes – Eric Jan 29 '15 at 16:48
  • **Not all formats of RFC3339 work with this code sample**, only if the second fraction part has 6 digits! So the first example on [page 9 section 5.8 of RFC 3339 version July 2002](https://www.rfc-editor.org/rfc/rfc3339#section-5.8) would not work: 1985-04-12T23:20:50.52Z --> false: 1985-04-12T23:20:50.**0000**52 I mention this, because the question seems related to RFC3339 and only provides an 6 digit second fraction number as a 'like' _example_ not telling that all date times contain always 6 digits, or always trailing zeros in the second fraction part (...59.999000Z or ...59.999Z ?). – BitLauncher Jan 17 '22 at 16:23
  • @BitLauncher You saved my sanity! I was trying all the combinations in this thread and you had the magic answer: You can only have 6 decimals after the decimal point!!! The timestamps I was working with had 9 decimals! Who would think that should make a difference between conversion and invalid format? I ended up using `fromisoformat(timestamp[:-4])` to keep it simple, and that worked fine! – RufusVS Sep 30 '22 at 18:05
22

In these days, Arrow also can be used as a third-party solution:

>>> import arrow
>>> date = arrow.get("2008-09-03T20:56:35.450686Z")
>>> date.datetime
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc())
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ilker Kesen
  • 483
  • 6
  • 11
21

Just use the python-dateutil module:

>>> import dateutil.parser as dp
>>> t = '1984-06-02T19:05:00.000Z'
>>> parsed_t = dp.parse(t)
>>> print(parsed_t)
datetime.datetime(1984, 6, 2, 19, 5, tzinfo=tzutc())

Documentation

Blairg23
  • 11,334
  • 6
  • 72
  • 72
  • `dateutil.parser.parse` will accept formats that are definitely _not_ ISO 8601, like `"Sat Oct 11 17:13:46 UTC 2003"`. If you specifically want ISO 8601 parsing, you would probably rather use `dateutil.parse.isoparse` instead, as [Flimms's answer recommends](https://stackoverflow.com/a/15228038/1709587). – Mark Amery Oct 09 '22 at 17:34
16

I have found ciso8601 to be the fastest way to parse ISO 8601 timestamps.

It also has full support for RFC 3339, and a dedicated function for strict parsing RFC 3339 timestamps.

Example usage:

>>> import ciso8601
>>> ciso8601.parse_datetime('2014-01-09T21')
datetime.datetime(2014, 1, 9, 21, 0)
>>> ciso8601.parse_datetime('2014-01-09T21:48:00.921000+05:30')
datetime.datetime(2014, 1, 9, 21, 48, 0, 921000, tzinfo=datetime.timezone(datetime.timedelta(seconds=19800)))
>>> ciso8601.parse_rfc3339('2014-01-09T21:48:00.921000+05:30')
datetime.datetime(2014, 1, 9, 21, 48, 0, 921000, tzinfo=datetime.timezone(datetime.timedelta(seconds=19800)))

The GitHub Repo README shows their speedup versus all of the other libraries listed in the other answers.

My personal project involved a lot of ISO 8601 parsing. It was nice to be able to just switch the call and go faster. :)

Edit: I have since become a maintainer of ciso8601. It's now faster than ever!

movermeyer
  • 1,502
  • 2
  • 13
  • 19
  • This looks like a great library! For those wanting to optimize ISO8601 parsing on Google App Engine, sadly, we can't use it since it's a C library, but your benchmarks were insightful to show that native `datetime.strptime()` is the next fastest solution. Thanks for putting all that info together! – hamx0r Jul 03 '18 at 17:18
  • 3
    @hamx0r, be aware that `datetime.strptime()` is not a full ISO 8601 parsing library. If you are on Python 3.7, you can use the `datetime.fromisoformat()` method, which is a little more flexible. You might be [interested in this more complete list of parsers](https://github.com/closeio/ciso8601/blob/benchmarking/README.rst#benchmark) which should be merged into the ciso8601 README soon. – movermeyer Jul 03 '18 at 19:50
  • ciso8601 works quite nice, but one have to first do "pip install pytz", because one cannot parse a timestamp with time zone information without the pytz dependency. Example would look like: dob = ciso8601.parse_datetime(result['dob']['date']) – d_- Jul 28 '18 at 11:20
  • 2
    @Dirk, [only in Python 2](https://github.com/closeio/ciso8601#dependency-on-pytz-python-2). But even that [should be removed](https://github.com/closeio/ciso8601/pull/58) in the next release. – movermeyer Jul 29 '18 at 17:41
13

If you are working with Django, it provides the dateparse module that accepts a bunch of formats similar to ISO format, including the time zone.

If you are not using Django and you don't want to use one of the other libraries mentioned here, you could probably adapt the Django source code for dateparse to your project.

Don Kirkby
  • 53,582
  • 27
  • 205
  • 286
12

If you don't want to use dateutil, you can try this function:

def from_utc(utcTime,fmt="%Y-%m-%dT%H:%M:%S.%fZ"):
    """
    Convert UTC time string to time.struct_time
    """
    # change datetime.datetime to time, return time.struct_time type
    return datetime.datetime.strptime(utcTime, fmt)

Test:

from_utc("2007-03-04T21:08:12.123Z")

Result:

datetime.datetime(2007, 3, 4, 21, 8, 12, 123000)
enchanter
  • 884
  • 8
  • 20
  • 5
    This answer relies upon hard-coding a particular UTC offset (namely "Z", which means +00:00) into the format string passed to `strptime`. This is a bad idea because it will fail to parse any datetime with a different UTC offset and raise an exception. See [my answer](http://stackoverflow.com/a/30696682/1709587) that describes how parsing RFC 3339 with strptime is in fact impossible. – Mark Amery Jun 07 '15 at 18:15
  • 1
    It's hard-coded but its sufficient for case when you need to parse zulu only. – Sasha Jul 27 '15 at 08:53
  • 1
    @alexander yes - which may be the case if, for instance, you know that your date string was generated with JavaScript's [`toISOString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) method. But there's no mention of the limitation to Zulu time dates in this answer, nor did the question indicate that that's all that's needed, and just using `dateutil` is usually equally convenient and less narrow in what it can parse. – Mark Amery Aug 20 '15 at 13:41
9

I've coded up a parser for the ISO 8601 standard and put it on GitHub: https://github.com/boxed/iso8601. This implementation supports everything in the specification except for durations, intervals, periodic intervals, and dates outside the supported date range of Python's datetime module.

Tests are included! :P

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
boxed
  • 3,895
  • 2
  • 24
  • 26
  • 3
    Generally, links to a tool or library [should be accompanied by usage notes, a specific explanation of how the linked resource is applicable to the problem, or some sample code](http://stackoverflow.com/a/251605/584192), or if possible all of the above. – Samuel Liew Sep 23 '18 at 04:05
8

This works for stdlib on Python 3.2 onwards (assuming all the timestamps are UTC):

from datetime import datetime, timezone, timedelta
datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ").replace(
    tzinfo=timezone(timedelta(0)))

For example,

>>> datetime.utcnow().replace(tzinfo=timezone(timedelta(0)))
... datetime.datetime(2015, 3, 11, 6, 2, 47, 879129, tzinfo=datetime.timezone.utc)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Benjamin Riggs
  • 648
  • 1
  • 7
  • 13
  • 2
    This answer relies upon hard-coding a particular UTC offset (namely "Z", which means +00:00) into the format string passed to `strptime`. This is a bad idea because it will fail to parse any datetime with a different UTC offset and raise an exception. See [my answer](http://stackoverflow.com/a/30696682/1709587) that describes how parsing RFC 3339 with strptime is in fact impossible. – Mark Amery Jun 07 '15 at 18:15
  • 1
    In theory, yes, this fails. In practice, I've never encountered an ISO 8601-formatted date that wasn't in Zulu time. For my very-occasional need, this works great and isn't reliant on some external library. – Benjamin Riggs Dec 29 '15 at 21:28
  • 4
    you could use `timezone.utc` instead of `timezone(timedelta(0))`. Also, the code works in Python 2.6+ (at least) if you [supply `utc` tzinfo object](http://stackoverflow.com/a/2331635/4279) – jfs Dec 31 '15 at 01:24
  • Doesn't matter if you've encountered it, it doesn't match the spec. – theannouncer Feb 25 '19 at 20:45
  • You can use the `%Z` for timezone in the most recent versions of Python. – sventechie Mar 26 '19 at 18:53
8

I'm the author of iso8601utils. It can be found on GitHub or on PyPI. Here's how you can parse your example:

>>> from iso8601utils import parsers
>>> parsers.datetime('2008-09-03T20:56:35.450686Z')
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
Matthew Moisen
  • 16,701
  • 27
  • 128
  • 231
Marc Wilson
  • 161
  • 1
  • 2
  • 5
7

One straightforward way to convert an ISO 8601-like date string to a UNIX timestamp or datetime.datetime object in all supported Python versions without installing third-party modules is to use the date parser of SQLite.

#!/usr/bin/env python
from __future__ import with_statement, division, print_function
import sqlite3
import datetime

testtimes = [
    "2016-08-25T16:01:26.123456Z",
    "2016-08-25T16:01:29",
]
db = sqlite3.connect(":memory:")
c = db.cursor()
for timestring in testtimes:
    c.execute("SELECT strftime('%s', ?)", (timestring,))
    converted = c.fetchone()[0]
    print("%s is %s after epoch" % (timestring, converted))
    dt = datetime.datetime.fromtimestamp(int(converted))
    print("datetime is %s" % dt)

Output:

2016-08-25T16:01:26.123456Z is 1472140886 after epoch
datetime is 2016-08-25 12:01:26
2016-08-25T16:01:29 is 1472140889 after epoch
datetime is 2016-08-25 12:01:29
Damian Yerrick
  • 4,602
  • 2
  • 26
  • 64
  • 14
    Thanks. This is disgusting. I love it. – wchargin Jan 31 '19 at 18:41
  • 1
    What an incredible, awesome, beautiful hack! Thanks! – Havok Feb 08 '20 at 07:31
  • 1
    Welcome to the Bad and the Ugly section. – Wolfgang Kuehn Sep 16 '21 at 14:04
  • Note that SQLite's date & time parsing is both more permissive than RFC 3339 and not permissive enough to handle all of ISO 8601, so it's not a perfect approach to parsing either format. Also, this is a hideous hack. But I suppose the fact that it avoids the need to install third party libraries is a virtue of sorts! – Mark Amery Oct 09 '22 at 17:09
7

Django's parse_datetime() function supports dates with UTC offsets:

parse_datetime('2016-08-09T15:12:03.65478Z') =
datetime.datetime(2016, 8, 9, 15, 12, 3, 654780, tzinfo=<UTC>)

So it could be used for parsing ISO 8601 dates in fields within entire project:

from django.utils import formats
from django.forms.fields import DateTimeField
from django.utils.dateparse import parse_datetime

class DateTimeFieldFixed(DateTimeField):
    def strptime(self, value, format):
        if format == 'iso-8601':
            return parse_datetime(value)
        return super().strptime(value, format)

DateTimeField.strptime = DateTimeFieldFixed.strptime
formats.ISO_INPUT_FORMATS['DATETIME_INPUT_FORMATS'].insert(0, 'iso-8601')
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
Artem Vasilev
  • 139
  • 1
  • 3
7

An another way is to use specialized parser for ISO-8601 is to use isoparse function of dateutil parser:

from dateutil import parser

date = parser.isoparse("2008-09-03T20:56:35.450686+01:00")
print(date)

Output:

2008-09-03 20:56:35.450686+01:00

This function is also mentioned in the documentation for the standard Python function datetime.fromisoformat:

A more full-featured ISO 8601 parser, dateutil.parser.isoparse is available in the third-party package dateutil.

zawuza
  • 854
  • 12
  • 16
5

If pandas is used anyway, I can recommend Timestamp from pandas. There you can

ts_1 = pd.Timestamp('2020-02-18T04:27:58.000Z')    
ts_2 = pd.Timestamp('2020-02-18T04:27:58.000')

Rant: It is just unbelievable that we still need to worry about things like date string parsing in 2021.

Michael Dorner
  • 17,587
  • 13
  • 87
  • 117
  • 1
    pandas is **strongly** discouraged for this simple case: It depends on pytz, which violates the python standard, and pd.Timestamp is subtly not a compatible datetime object. – Wolfgang Kuehn Sep 16 '21 at 13:56
  • Thanks for your comment. Do you have some pointers for me? I was not able to find pytz: https://github.com/pandas-dev/pandas/blob/7036de35378d9db6236de2d70fe5f104b0bcdc9c/pandas/_libs/tslibs/timestamps.pyi#L31 and I’m not sure what Python standard and its violation you are referring to. – Michael Dorner Sep 16 '21 at 22:00
  • 2
    See the [rant by Paul Ganssle](https://github.com/pandas-dev/pandas/issues/37654). As for incompatibility, execute both `datetime.fromisoformat('2021-01-01T00:00:00+01:00').tzinfo.utc` and `pandas.Timestamp('2021-01-01T00:00:00+01:00').tzinfo.utc` : Not the same at all. – Wolfgang Kuehn Sep 17 '21 at 07:58
  • Thank you for pointers to this ongoing work. I didn’t know about that issue, but I really hope they fix it soon! But again: I can’t believe that time parsing is still an issue. :-) – Michael Dorner Sep 18 '21 at 10:46
3

Because ISO 8601 allows many variations of optional colons and dashes being present, basically CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]. If you want to use strptime, you need to strip out those variations first.

The goal is to generate a utc datetime object.


If you just want a basic case that work for UTC with the Z suffix like 2016-06-29T19:36:29.3453Z:
datetime.datetime.strptime(timestamp.translate(None, ':-'), "%Y%m%dT%H%M%S.%fZ")


If you want to handle timezone offsets like 2016-06-29T19:36:29.3453-0400 or 2008-09-03T20:56:35.450686+05:00 use the following. These will convert all variations into something without variable delimiters like 20080903T205635.450686+0500 making it more consistent/easier to parse.
import re
# this regex removes all colons and all 
# dashes EXCEPT for the dash indicating + or - utc offset for the timezone
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', timestamp)
datetime.datetime.strptime(conformed_timestamp, "%Y%m%dT%H%M%S.%f%z" )


If your system does not support the %z strptime directive (you see something like ValueError: 'z' is a bad directive in format '%Y%m%dT%H%M%S.%f%z') then you need to manually offset the time from Z (UTC). Note %z may not work on your system in python versions < 3 as it depended on the c library support which varies across system/python build type (i.e. Jython, Cython, etc.).
import re
import datetime

# this regex removes all colons and all 
# dashes EXCEPT for the dash indicating + or - utc offset for the timezone
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', timestamp)

# split on the offset to remove it. use a capture group to keep the delimiter
split_timestamp = re.split(r"[+|-]",conformed_timestamp)
main_timestamp = split_timestamp[0]
if len(split_timestamp) == 3:
    sign = split_timestamp[1]
    offset = split_timestamp[2]
else:
    sign = None
    offset = None

# generate the datetime object without the offset at UTC time
output_datetime = datetime.datetime.strptime(main_timestamp +"Z", "%Y%m%dT%H%M%S.%fZ" )
if offset:
    # create timedelta based on offset
    offset_delta = datetime.timedelta(hours=int(sign+offset[:-2]), minutes=int(sign+offset[-2:]))
    # offset datetime with timedelta
    output_datetime = output_datetime + offset_delta
theannouncer
  • 1,148
  • 16
  • 28
  • This is broken; some quick experimentation shows it raises an exception if `timestamp` is `'2016-06-29T19:36:29.123Z'` or `'2016-06-29T19:36:29+00:00'`, both of which are valid RFC 3339 and ISO 8601 datetimes. – Mark Amery Oct 09 '22 at 16:45
3

Nowadays there's Maya: Datetimes for Humans™, from the author of the popular Requests: HTTP for Humans™ package:

>>> import maya
>>> str = '2008-09-03T20:56:35.450686Z'
>>> maya.MayaDT.from_rfc3339(str).datetime()
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=<UTC>)
jrc
  • 20,354
  • 10
  • 69
  • 64
2

The python-dateutil will throw an exception if parsing invalid date strings, so you may want to catch the exception.

from dateutil import parser
ds = '2012-60-31'
try:
  dt = parser.parse(ds)
except ValueError, e:
  print '"%s" is an invalid date' % ds
2

datetime.fromisoformat() is improved in Python 3.11 to parse most ISO 8601 formats

datetime.fromisoformat() can now be used to parse most ISO 8601 formats, barring only those that support fractional hours and minutes. Previously, this method only supported formats that could be emitted by datetime.isoformat().

>>> from datetime import datetime
>>> datetime.fromisoformat('2011-11-04T00:05:23Z')
datetime.datetime(2011, 11, 4, 0, 5, 23, tzinfo=datetime.timezone.utc)
>>> datetime.fromisoformat('20111104T000523')
datetime.datetime(2011, 11, 4, 0, 5, 23)
>>> datetime.fromisoformat('2011-W01-2T00:05:23.283')
datetime.datetime(2011, 1, 4, 0, 5, 23, 283000)
Ash Nazg
  • 514
  • 3
  • 6
  • 14
1

Thanks to great Mark Amery's answer I devised function to account for all possible ISO formats of datetime:

class FixedOffset(tzinfo):
    """Fixed offset in minutes: `time = utc_time + utc_offset`."""
    def __init__(self, offset):
        self.__offset = timedelta(minutes=offset)
        hours, minutes = divmod(offset, 60)
        #NOTE: the last part is to remind about deprecated POSIX GMT+h timezones
        #  that have the opposite sign in the name;
        #  the corresponding numeric value is not used e.g., no minutes
        self.__name = '<%+03d%02d>%+d' % (hours, minutes, -hours)
    def utcoffset(self, dt=None):
        return self.__offset
    def tzname(self, dt=None):
        return self.__name
    def dst(self, dt=None):
        return timedelta(0)
    def __repr__(self):
        return 'FixedOffset(%d)' % (self.utcoffset().total_seconds() / 60)
    def __getinitargs__(self):
        return (self.__offset.total_seconds()/60,)

def parse_isoformat_datetime(isodatetime):
    try:
        return datetime.strptime(isodatetime, '%Y-%m-%dT%H:%M:%S.%f')
    except ValueError:
        pass
    try:
        return datetime.strptime(isodatetime, '%Y-%m-%dT%H:%M:%S')
    except ValueError:
        pass
    pat = r'(.*?[+-]\d{2}):(\d{2})'
    temp = re.sub(pat, r'\1\2', isodatetime)
    naive_date_str = temp[:-5]
    offset_str = temp[-5:]
    naive_dt = datetime.strptime(naive_date_str, '%Y-%m-%dT%H:%M:%S.%f')
    offset = int(offset_str[-4:-2])*60 + int(offset_str[-2:])
    if offset_str[0] == "-":
        offset = -offset
    return naive_dt.replace(tzinfo=FixedOffset(offset))
Community
  • 1
  • 1
omikron
  • 2,745
  • 1
  • 25
  • 34
1

For something that works with the 2.X standard library try:

calendar.timegm(time.strptime(date.split(".")[0]+"UTC", "%Y-%m-%dT%H:%M:%S%Z"))

calendar.timegm is the missing gm version of time.mktime.

Gordon Wrigley
  • 11,015
  • 10
  • 48
  • 62
  • 2
    This just ignores the timezone '2013-01-28T14:01:01.335612-08:00' --> parsed as UTC, not PDT – gatoatigrado Jan 29 '13 at 01:31
  • Besides ignoring the timezone as @gatoatigrado notes, this also raises an exception if you give it input which has a timezone but _not_ a fractional number of seconds (and thus no `.` character), like `2022-10-09T15:49:22-07:00`. Such a value is a valid RFC 3339 and ISO 8601 date time string, so a parser shouldn't choke on it. – Mark Amery Oct 09 '22 at 15:54
-1

Initially I tried with:

from operator import neg, pos
from time import strptime, mktime
from datetime import datetime, tzinfo, timedelta

class MyUTCOffsetTimezone(tzinfo):
    @staticmethod
    def with_offset(offset_no_signal, signal):  # type: (str, str) -> MyUTCOffsetTimezone
        return MyUTCOffsetTimezone((pos if signal == '+' else neg)(
            (datetime.strptime(offset_no_signal, '%H:%M') - datetime(1900, 1, 1))
          .total_seconds()))

    def __init__(self, offset, name=None):
        self.offset = timedelta(seconds=offset)
        self.name = name or self.__class__.__name__

    def utcoffset(self, dt):
        return self.offset

    def tzname(self, dt):
        return self.name

    def dst(self, dt):
        return timedelta(0)


def to_datetime_tz(dt):  # type: (str) -> datetime
    fmt = '%Y-%m-%dT%H:%M:%S.%f'
    if dt[-6] in frozenset(('+', '-')):
        dt, sign, offset = strptime(dt[:-6], fmt), dt[-6], dt[-5:]
        return datetime.fromtimestamp(mktime(dt),
                                      tz=MyUTCOffsetTimezone.with_offset(offset, sign))
    elif dt[-1] == 'Z':
        return datetime.strptime(dt, fmt + 'Z')
    return datetime.strptime(dt, fmt)

But that didn't work on negative timezones. This however I got working fine, in Python 3.7.3:

from datetime import datetime


def to_datetime_tz(dt):  # type: (str) -> datetime
    fmt = '%Y-%m-%dT%H:%M:%S.%f'
    if dt[-6] in frozenset(('+', '-')):
        return datetime.strptime(dt, fmt + '%z')
    elif dt[-1] == 'Z':
        return datetime.strptime(dt, fmt + 'Z')
    return datetime.strptime(dt, fmt)

Some tests, note that the out only differs by precision of microseconds. Got to 6 digits of precision on my machine, but YMMV:

for dt_in, dt_out in (
        ('2019-03-11T08:00:00.000Z', '2019-03-11T08:00:00'),
        ('2019-03-11T08:00:00.000+11:00', '2019-03-11T08:00:00+11:00'),
        ('2019-03-11T08:00:00.000-11:00', '2019-03-11T08:00:00-11:00')
    ):
    isoformat = to_datetime_tz(dt_in).isoformat()
    assert isoformat == dt_out, '{} != {}'.format(isoformat, dt_out)
A T
  • 13,008
  • 21
  • 97
  • 158
  • 1
    May I ask why did you do `frozenset(('+', '-'))`? Shouldn't a normal tuple like `('+', '-')` be able to accomplish the same thing? – Prahlad Yeri Jun 08 '19 at 13:46
  • Sure, but isn't that a linear scan rather than a perfectly hashed lookup? – A T Jun 08 '19 at 14:16
  • At least a couple of bugs in your `to_datetime_tz` function: 1. datetime strings without a decimal point in the seconds (like `2019-03-11T08:00:00+11:00`) trigger exceptions despite being valid ISO 8601 and RFC 3339 datetimes, and 2. timezone offset `Z` is treated differently from `+00:00` even though they are supposed to mean the same thing. – Mark Amery Oct 09 '22 at 16:15
  • As for @PrahladYeri's point about the frozenset, Prahlad is quite right. There's no way the `frozenset` lookup is gonna be faster with only two items, especially when you're actually having to construct and iterate over an equivalent 2-item tuple anyway as part of the construction of the `frozenset`. And even if it were faster, the cost of doing a lookup in a 2-item collection is never gonna matter. – Mark Amery Oct 09 '22 at 16:17
  • I suppose you can do length checks on the input string to determine what's in it. You're welcome to edit this answer from > 3 years ago. – A T Oct 15 '22 at 23:33
-2
def parseISO8601DateTime(datetimeStr):
    import time
    from datetime import datetime, timedelta

    def log_date_string(when):
        gmt = time.gmtime(when)
        if time.daylight and gmt[8]:
            tz = time.altzone
        else:
            tz = time.timezone
        if tz > 0:
            neg = 1
        else:
            neg = 0
            tz = -tz
        h, rem = divmod(tz, 3600)
        m, rem = divmod(rem, 60)
        if neg:
            offset = '-%02d%02d' % (h, m)
        else:
            offset = '+%02d%02d' % (h, m)

        return time.strftime('%d/%b/%Y:%H:%M:%S ', gmt) + offset

    dt = datetime.strptime(datetimeStr, '%Y-%m-%dT%H:%M:%S.%fZ')
    timestamp = dt.timestamp()
    return dt + timedelta(hours=dt.hour-time.gmtime(timestamp).tm_hour)

Note that we should look if the string doesn't ends with Z, we could parse using %z.

Denny Weinberg
  • 2,492
  • 1
  • 21
  • 34