511

This question already has an answer here:

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?

marked as duplicate by tripleee, jww, Madhur Bhaiya, Pearly Spencer, Paul Roub Nov 12 '18 at 15:48

This question has been asked before and already has an answer. If those answers do not fully address your question, please ask a new question.

24 Answers 24

359

The python-dateutil package can 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.parse('2008-09-03T20:56:35.450686Z') # RFC 3339 format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc())
>>> dateutil.parser.parse('2008-09-03T20:56:35.450686') # ISO 8601 extended format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
>>> dateutil.parser.parse('20080903T205635.450686') # ISO 8601 basic format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
>>> dateutil.parser.parse('20080903') # ISO 8601 basic format, date only
datetime.datetime(2008, 9, 3, 0, 0)

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. (thanks ivan_pozdeev)

The Pypi name is python-dateutil, not dateutil (thanks code3monk3y):

pip install python-dateutil

If you're using Python 3.7, have a look at this answer about datetime.datetime.fromisoformat.

  • 67
    For the lazy, it's installed via python-dateutil not dateutil, so: pip install python-dateutil. – cod3monk3y Mar 12 '14 at 21:55
  • 25
    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
  • 1
    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
  • @ivan_pozdeev what package would you recommend for non-guessing parsing? – bgusach Jan 10 '18 at 12:54
  • 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. – Jeremy Phelps Oct 1 '18 at 23:10
135

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

  • 4
    Note - if using Naive datetimes - I think you get no TZ at all - Z may not match anything. – Danny Staple Feb 2 '15 at 17:08
  • 16
    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. See my answer that describes how parsing RFC 3339 with strptime is in fact impossible. – Mark Amery Jun 7 '15 at 17:59
  • 1
    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 9 '16 at 5:33
  • Does Py3K mean Python 3000?!? – Robino Nov 13 '17 at 15:35
  • 2
    @Robino IIRC, "Python 3000" is an old name for what is now known as Python 3. – Jeremy Phelps Oct 1 '18 at 23:14
121

Several answers here suggest using datetime.datetime.strptime to parse RFC 3339 or ISO 8601 datetimes with timezones, like the one exhibited in the question:

2008-09-03T20:56:35.450686Z

This is a bad idea.

Assuming that you want to support the full RFC 3339 format, including support for UTC offsets other than zero, then the code these answers suggest does not work. Indeed, it cannot work, because parsing RFC 3339 syntax using strptime is impossible. The format strings used by Python's datetime module are incapable of describing RFC 3339 syntax.

The problem is UTC offsets. The RFC 3339 Internet Date/Time Format requires that every date-time includes a UTC offset, and that those offsets can either be Z (short for "Zulu time") or in +HH:MM or -HH:MM format, like +05:00 or -10:30.

Consequently, these are all valid RFC 3339 datetimes:

  • 2008-09-03T20:56:35.450686Z
  • 2008-09-03T20:56:35.450686+05:00
  • 2008-09-03T20:56:35.450686-10:30

Alas, the format strings used by strptime and strftime have no directive that corresponds to UTC offsets in RFC 3339 format. A complete list of the directives they support can be found at https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior, and the only UTC offset directive included in the list is %z:

%z

UTC offset in the form +HHMM or -HHMM (empty string if the the object is naive).

Example: (empty), +0000, -0400, +1030

This doesn't match the format of an RFC 3339 offset, and indeed if we try to use %z in the format string and parse an RFC 3339 date, we'll fail:

>>> from datetime import datetime
>>> datetime.strptime("2008-09-03T20:56:35.450686Z", "%Y-%m-%dT%H:%M:%S.%f%z")
Traceback (most recent call last):
  File "", line 1, in 
  File "/usr/lib/python3.4/_strptime.py", line 500, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File "/usr/lib/python3.4/_strptime.py", line 337, in _strptime
    (data_string, format))
ValueError: time data '2008-09-03T20:56:35.450686Z' does not match format '%Y-%m-%dT%H:%M:%S.%f%z'
>>> datetime.strptime("2008-09-03T20:56:35.450686+05:00", "%Y-%m-%dT%H:%M:%S.%f%z")
Traceback (most recent call last):
  File "", line 1, in 
  File "/usr/lib/python3.4/_strptime.py", line 500, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File "/usr/lib/python3.4/_strptime.py", line 337, in _strptime
    (data_string, format))
ValueError: time data '2008-09-03T20:56:35.450686+05:00' does not match format '%Y-%m-%dT%H:%M:%S.%f%z'

(Actually, the above is just what you'll see in Python 3. In Python 2 we'll fail for an even simpler reason, which is that strptime does not implement the %z directive at all in Python 2.)

The multiple answers here that recommend strptime all work around this by including a literal Z in their format string, which matches the Z from the question asker's example datetime string (and discards it, producing a datetime object without a timezone):

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

Since this discards timezone information that was included in the original datetime string, it's questionable whether we should regard even this result as correct. But more importantly, because this approach involves hard-coding a particular UTC offset into the format string, it will choke the moment it tries to parse any RFC 3339 datetime with a different UTC offset:

>>> datetime.strptime("2008-09-03T20:56:35.450686+05:00", "%Y-%m-%dT%H:%M:%S.%fZ")
Traceback (most recent call last):
  File "", line 1, in 
  File "/usr/lib/python3.4/_strptime.py", line 500, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File "/usr/lib/python3.4/_strptime.py", line 337, in _strptime
    (data_string, format))
ValueError: time data '2008-09-03T20:56:35.450686+05:00' does not match format '%Y-%m-%dT%H:%M:%S.%fZ'

Unless you're certain that you only need to support RFC 3339 datetimes in Zulu time, and not ones with other timezone offsets, don't use strptime. Use one of the many other approaches described in answers here instead.

  • 50
    It's mind bogging why strptime doesn't have a directive for ISO format timezone info, and why it cannot be parsed. Incredible. – Csaba Toth Sep 18 '15 at 17:43
  • 2
    @CsabaToth Entirely agreed - if I have some time to kill, perhaps I'll try to add it into the language. Or you could do so, if you were so inclined - I see you have some C experience, unlike me. – Mark Amery Sep 18 '15 at 17:45
  • 1
    @CsabaToth - Why incredible? It works good enough for most people, or they found easy enough workaround. If you need the feature, it is opensource and you can add it. Or pay someone to do it for you. Why someone should volunteer his own free time to solve your specific problems? Let source be with you. – Peter M. Jan 6 '16 at 14:42
  • As this basically means you can't reliably parse ISO 8601 dates using pure python (definitely not across python 2 and 3), I ended up using the popular arrow library instead: arrow.readthedocs.io/en/latest – Joris Dec 3 '16 at 15:14
  • 2
    @PeterMasiar Incredible because usually one discovers that things in python have been implemented thoughtfully and fully. We have been spoilt by this attention to detail and so when we stumble across something in the language that is "unpythonic" we throw our toys out the pram, as I am about to do so right now. Whaaaaaaaaaa Whaa wahaaaaa :-( – Robino Nov 13 '17 at 15:41
67

Try the iso8601 module; it does exactly this.

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

  • 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 8:56
  • 2
    Be aware that the pip version of iso8601 hasn't been updated since 2007 and has some serious bugs that are outstanding. I recommend applying some critical of the patches yourself or find one of the many github forks that have already done so github.com/keithhackbarth/pyiso8601-strict – keithhackbarth Jun 24 '13 at 19:10
  • 6
    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 0:50
62

New in Python 3.7+


The datetime standard library introduced a function for inverting datetime.isoformat().

classmethod datetime.fromisoformat(date_string):

Return a datetime corresponding to a date_string in one of the formats emitted by date.isoformat() and datetime.isoformat().

Specifically, this function supports strings in the format(s):

YYYY-MM-DD[*HH[:MM[:SS[.mmm[mmm]]]][+HH:MM[:SS[.ffffff]]]]

where * can match any single character.

Caution: This does not support parsing arbitrary ISO 8601 strings - it is only intended as the inverse operation of datetime.isoformat().

Example of use:

from datetime import datetime

date = datetime.fromisoformat('2017-01-01T12:30:59.000000')
  • 2
    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
  • 5
    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
  • 2
    To properly support the Z the input script can be modified with date_string.replace("Z", "+00:00"). – jox Dec 2 '18 at 10:47
35
import re,datetime
s="2008-09-03T20:56:35.450686Z"
d=datetime.datetime(*map(int, re.split('[^\d]', s)[:-1]))
  • 69
    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
  • 14
    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
  • 2
    This is equivalent of d=datetime.datetime(*map(int, re.split('\D', s)[:-1])) i suppose. – Xuan May 21 '13 at 9:18
  • 3
    a variation: datetime.datetime(*map(int, re.findall('\d+', s)) – jfs May 16 '14 at 2:19
  • 3
    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
27

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)
  • 7
    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 4 '11 at 22:24
  • 8
    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 7 '15 at 18:12
19

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

import datetime
datetime.datetime.strptime('2018-01-31T09:24:31.488670+00:00', '%Y-%m-%dT%H:%M:%S.%f%z')
  • 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 at 12:53
19

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())
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)
  • 4
    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 that describes how parsing RFC 3339 with strptime is in fact impossible. – Mark Amery Jun 7 '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 8:53
  • @alexander yes - which may be the case if, for instance, you know that your date string was generated with JavaScript's 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
11

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.

10

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

7

I have found ciso8601 to be the fastest way to parse ISO 8601 timestamps. As the name suggests, it is implemented in C.

import ciso8601
ciso8601.parse_datetime('2014-01-09T21:48:00.921000+05:30')

The GitHub Repo README shows their >10x 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 10x faster. :)

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

  • 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 3 '18 at 17:18
  • 1
    @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 which should be merged into the ciso8601 README soon. – movermeyer Jul 3 '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']) – Dirk Jul 28 '18 at 11:20
  • 1
    @Dirk, only in Python 2. But even that should be removed in the next release. – movermeyer Jul 29 '18 at 17:41
7

I'm the author of iso8601 utils. 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)
6

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
6

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

5

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')
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
2

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.

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

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>)
2

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)
  • 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 that describes how parsing RFC 3339 with strptime is in fact impossible. – Mark Amery Jun 7 '15 at 18:15
  • 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
  • 3
    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 – jfs Dec 31 '15 at 1:24
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))
0
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.

Not the answer you're looking for? Browse other questions tagged or ask your own question.