474

I'm getting a datetime string in a format like "2009-05-28T16:15:00" (this is ISO 8601, I believe). One hackish option seems to be to parse the string using time.strptime and passing the first six elements of the tuple into the datetime constructor, like:

datetime.datetime(*time.strptime("2007-03-04T21:08:12", "%Y-%m-%dT%H:%M:%S")[:6])

I haven't been able to find a "cleaner" way of doing this. Is there one?

Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
Andrey Fedorov
  • 7,836
  • 20
  • 63
  • 95
  • 18
    It's worth bearing in mind that this isn't *quite* a duplicate of the issue it's been closed against. The linked issue refers specifically to [RFC 3339](https://tools.ietf.org/html/rfc3339) strings, while this one refers to ISO 8601 strings. The RFC 3339 syntax is a subset of the ISO 8601 syntax (defined in the non-free ISO 8601 standard which, like most ISO standards, you must either pirate or pay a huge fee to read). The datetime string exhibited in this question is an ISO 8601 datetime, but NOT an RFC 3339 datetime. UTC offsets are mandatory in RFC 3339 datetimes, and none is provided here. – Mark Amery Jun 07 '15 at 16:20

11 Answers11

771

I prefer using the dateutil library for timezone handling and generally solid date parsing. If you were to get an ISO 8601 string like: 2010-05-08T23:41:54.000Z you'd have a fun time parsing that with strptime, especially if you didn't know up front whether or not the timezone was included. pyiso8601 has a couple of issues (check their tracker) that I ran into during my usage and it hasn't been updated in a few years. dateutil, by contrast, has been active and worked for me:

import dateutil.parser
yourdate = dateutil.parser.parse(datestring)
Nicolas Gervais
  • 21,923
  • 10
  • 61
  • 96
Wes Winham
  • 1,750
  • 3
  • 16
  • 16
  • 5
    right, pyiso8601 has some very subtle issues which you might notice when it's already spread over the entire code. dateutil.parser is really good, but one should keep an eye of enforcing tz-awareness manually if necessary. – Daniel F Sep 22 '13 at 09:09
  • 5
    An update to pyiso8601 in early Feb 2014 has resolved many issues. It handles a much broader set of valid ISO8601 strings. It is worth another look. – Dave Hein Nov 13 '14 at 00:46
  • 3
    I've been pulling my hair out with a ton of `elif`s trying to use `datetime.datetime.strptime` to handle all my various datetime formats. THANK YOU for showing me the light. – punkrockpolly Jan 14 '15 at 00:11
  • 4
    Correct me if I'm wrong, but doesn't the Z in the time example you include specifically indicate a UTC time? – dicroce Aug 28 '16 at 18:30
  • @dicroce: exactly, but it's not always there. `2010-05-08T23:41:54.000Z` and `2010-05-08T23:41:54.000` are both valid ISO 8601 strings, but writing code that uses `strptime` to handle all possibilities is a PITA. You can read the ISO standard, put together a great long list and try a whole load of formats in turn, or you can just use a library... – Steve Jessop Sep 01 '16 at 15:09
  • [**`arrow`**](https://arrow.readthedocs.io/en/latest/) do the work out of the box, there are enough useful examples, and good documentation. `dateutil` can't handle even timezone correctly (out of the box). Probably, **`arrow`** is a better choice. – maxkoryukov Jun 06 '17 at 19:59
  • 94
    As from python 3.7 you can use `datetime.datetime.fromisoformat` https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat – Yuri Ritvin Sep 17 '18 at 14:15
  • 1
    dateutil.parser is very buggy. The following parsing gives different results. The first one is the default germany DD.MM.YYYY format. Parsing in an german enviroment gives different results. >>> dateutil.parser.parse("01.02.2017") datetime.datetime(2017, 1, 2, 0, 0) >>> dateutil.parser.parse("2017-01-02") datetime.datetime(2017, 1, 2, 0, 0) – ego2dot0 Nov 23 '18 at 14:28
  • >>> dateutil.parser.parse("01.31.2017") -> datetime.datetime(2017, 1, 31, 0, 0) >>> dateutil.parser.parse("31.01.2017") -> datetime.datetime(2017, 1, 31, 0, 0) – ego2dot0 Nov 23 '18 at 14:34
  • The code in the answer doesn't actually work for me. You can't import dateutil.parser in Python 3.6.7. That property does not exist. You can: `from dateutil.parser import parse` however, which is what I've done. – rjurney Apr 18 '19 at 22:37
  • 29
    @YuriRitvin: The official documentation from the [link](https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat) you gave reads the following **caution**: _This does not support parsing arbitrary ISO 8601 strings - it is only intended as the inverse operation of datetime.isoformat(). A more full-featured ISO 8601 parser, dateutil.parser.isoparse is available in the third-party package dateutil_. So yeah, even for Python 3.7 we are back to the `dateutil` package. – Voicu Oct 03 '19 at 16:52
152

Since Python 3.7 and no external libraries, you can use the strptime function from the datetime module:

datetime.datetime.strptime('2019-01-04T16:41:24+0200', "%Y-%m-%dT%H:%M:%S%z")

For more formatting options, see here.

Python 2 doesn't support the %z format specifier, so it's best to explicitly use Zulu time everywhere if possible:

datetime.datetime.strptime("2007-03-04T21:08:12Z", "%Y-%m-%dT%H:%M:%SZ")
Corey Cole
  • 1,562
  • 13
  • 35
Philippe F
  • 10,569
  • 5
  • 27
  • 29
  • 3
    Perhaps you were looking the datetime module level functions, instead of the datetime.datetime class methods. – tzot Jun 11 '09 at 00:26
  • 24
    You gotta agree though that this contradicts python ideology, being rather unobvious... `strptime`? Couldn't they use a meaningful name rather than propagate an old crappy C name?... – Roman Starkov Jan 18 '10 at 01:00
  • 6
    Note that this parses a subset of ISO 8601. If you tell your client that you can parse all 8601 datetimes, they may send you one without dashes, without colons, with a weeknumer instead of a month, etc. – Peter Oct 02 '13 at 16:08
  • My answer [above](http://stackoverflow.com/a/12184365/738924), adds the timezone. ( note the .%fZ) – theannouncer Dec 11 '15 at 21:46
  • Does not parse second decimals (e.g. 21:08:12.345) and does not parse timezone. – Robino Nov 13 '17 at 15:28
  • 2
    Downvoted because the question specifically say ISO-8601, while avoiding explicit string format. I am specifically trying to find an answer without having to how explicit string format. Also Python's `str(datetime)` and JavaScript's `Date.toISOString()` is a little different. – Polv Jul 21 '19 at 08:13
  • 1
    Attention! This (%z) works only since python 3.7 – Alex Dembo Aug 08 '19 at 06:26
  • %z works for me in 3.6.8. Other pages say it should work from 3.2 onwards. But please try before trusting it blindly. – JerkMan Nov 19 '19 at 15:55
61

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
  • 935
  • 13
  • 24
  • 1
    Note, this deals with timezones appropriately ( note the .%fZ) – theannouncer Dec 11 '15 at 21:45
  • 1
    This will fail on valid ISO 8601 datetime like `20160628T100000`. – Seppo Erviälä Jun 28 '16 at 13:01
  • @SeppoErviälä, good point. I updated my answer. – theannouncer Jun 28 '16 at 19:50
  • Note, the updated answer also deals with the Z +HH:MM or -HH:MM UTC offset format, like +05:00 or -10:30 which @MarkAmery mentioned here as it strips out the colons – theannouncer Jun 29 '16 at 19:50
  • 6
    Oh dear, Python. What the hell are you doing?!? – Robino Nov 13 '17 at 15:29
  • Excellent example, this helped me with a little datetime conversion I was working on. Thanks – Helen Neely May 30 '19 at 09:08
  • there is a bug caused by a syntax error of the regular expression in the update as sthe split does not capture the delimiter - line ´split_timestamp = re.split(r"[+|-]",conformed_timestamp)´ must be `split_timestamp = re.split(r"([+|-])",conformed_timestamp)` – mhwh Jul 22 '20 at 09:15
  • 1
    @mhwh good catch, I have updated the code – theannouncer Jul 23 '20 at 01:18
  • 1
    relevant docs: `If capturing parentheses are used in pattern, then the text of all groups in the pattern are also returned as part of the resulting list.` . LOL bc i literally had the comment `Use a capture group to keep the delimiter` just above. – theannouncer Jul 23 '20 at 01:19
42

Arrow looks promising for this:

>>> import arrow
>>> arrow.get('2014-11-13T14:53:18.694072+00:00').datetime
datetime.datetime(2014, 11, 13, 14, 53, 18, 694072, tzinfo=tzoffset(None, 0))

Arrow is a Python library that provides a sensible, intelligent way of creating, manipulating, formatting and converting dates and times. Arrow is simple, lightweight and heavily inspired by moment.js and requests.

Saransh Singh
  • 520
  • 3
  • 11
Avi Flax
  • 46,847
  • 9
  • 42
  • 61
17

You should keep an eye on the timezone information, as you might get into trouble when comparing non-tz-aware datetimes with tz-aware ones.

It's probably the best to always make them tz-aware (even if only as UTC), unless you really know why it wouldn't be of any use to do so.

#-----------------------------------------------
import datetime
import pytz
import dateutil.parser
#-----------------------------------------------

utc = pytz.utc
BERLIN = pytz.timezone('Europe/Berlin')
#-----------------------------------------------

def to_iso8601(when=None, tz=BERLIN):
  if not when:
    when = datetime.datetime.now(tz)
  if not when.tzinfo:
    when = tz.localize(when)
  _when = when.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
  return _when[:-8] + _when[-5:] # Remove microseconds
#-----------------------------------------------

def from_iso8601(when=None, tz=BERLIN):
  _when = dateutil.parser.parse(when)
  if not _when.tzinfo:
    _when = tz.localize(_when)
  return _when
#-----------------------------------------------
Daniel F
  • 11,845
  • 6
  • 75
  • 100
9

I haven't tried it yet, but pyiso8601 promises to support this.

user
  • 4,909
  • 7
  • 43
  • 60
Avi Flax
  • 46,847
  • 9
  • 42
  • 61
  • 4
    pyiso8601 has a _very_ limited range of formats which it accepts. better use dateutil.parser --> "Currently the following formats are handled: 1) 2006-01-01T00:00:00Z 2) 2006-01-01T00:00:00[+-]00:00" Having [+-]0000 as tz-information is just as valid under the iso standard. IIRC on [+-]0000 it would just discard the tz-information... – Daniel F Sep 22 '13 at 09:15
  • 1
    [pyiso8601](https://pypi.python.org/pypi/iso8601/) has been updated recently (circa Feb 2014) and now handles [+-]0000. It also handles just dates. I've been using pyiso8601 to good effect. – Dave Hein Nov 13 '14 at 00:43
7
import datetime, time
def convert_enddate_to_seconds(self, ts):
    """Takes ISO 8601 format(string) and converts into epoch time."""
    dt = datetime.datetime.strptime(ts[:-7],'%Y-%m-%dT%H:%M:%S.%f')+\
                datetime.timedelta(hours=int(ts[-5:-3]),
                minutes=int(ts[-2:]))*int(ts[-6:-5]+'1')
    seconds = time.mktime(dt.timetuple()) + dt.microsecond/1000000.0
    return seconds

This also includes the milliseconds and time zone.

If the time is '2012-09-30T15:31:50.262-08:00', this will convert into epoch time.

>>> import datetime, time
>>> ts = '2012-09-30T15:31:50.262-08:00'
>>> dt = datetime.datetime.strptime(ts[:-7],'%Y-%m-%dT%H:%M:%S.%f')+ datetime.timedelta(hours=int(ts[-5:-3]), minutes=int(ts[-2:]))*int(ts[-6:-5]+'1')
>>> seconds = time.mktime(dt.timetuple()) + dt.microsecond/1000000.0
>>> seconds
1348990310.26
zmo
  • 22,917
  • 4
  • 48
  • 82
ronak
  • 1,492
  • 2
  • 17
  • 32
6

Both ways:

Epoch to ISO time:

isoTime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(epochTime))

ISO time to Epoch:

epochTime = time.mktime(time.strptime(isoTime, '%Y-%m-%dT%H:%M:%SZ'))
billmanH
  • 962
  • 1
  • 11
  • 21
  • 2
    but you are limited to UTC only (z) – confiq Jun 19 '16 at 09:03
  • 2
    Parses neither decimal seconds nor timezones (other than "Z") – Robino Nov 13 '17 at 16:58
  • Good point, however you could modify the text string params to fit your specific format. https://docs.python.org/2/library/time.html You just need to manipulate the string to fit your input. – billmanH Nov 13 '17 at 18:13
  • 1
    Moreover, you can set arbitrary format instead of ISO: `time.strftime("%d-%m-%y %H:%M", time.localtime(EPOCH_TIME))`. – whtyger Jan 17 '19 at 11:54
6

Isodate seems to have the most complete support.

Tobu
  • 23,001
  • 3
  • 85
  • 97
4

aniso8601 should handle this. It also understands timezones, Python 2 and Python 3, and it has a reasonable coverage of the rest of ISO 8601, should you ever need it.

import aniso8601
aniso8601.parse_datetime('2007-03-04T21:08:12')
Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
-16

Here is a super simple way to do these kind of conversions. No parsing, or extra libraries required. It is clean, simple, and fast.

import datetime
import time

################################################
#
# Takes the time (in seconds),
#   and returns a string of the time in ISO8601 format.
# Note: Timezone is UTC
#
################################################

def TimeToISO8601(seconds):
   strKv = datetime.datetime.fromtimestamp(seconds).strftime('%Y-%m-%d')
   strKv = strKv + "T"
   strKv = strKv + datetime.datetime.fromtimestamp(seconds).strftime('%H:%M:%S')
   strKv = strKv +"Z"
   return strKv

################################################
#
# Takes a string of the time in ISO8601 format,
#   and returns the time (in seconds).
# Note: Timezone is UTC
#
################################################

def ISO8601ToTime(strISOTime):
   K1 = 0
   K2 = 9999999999
   K3 = 0
   counter = 0
   while counter < 95:
     K3 = (K1 + K2) / 2
     strK4 = TimeToISO8601(K3)
     if strK4 < strISOTime:
       K1 = K3
     if strK4 > strISOTime:
       K2 = K3
     counter = counter + 1
   return K3

################################################
#
# Takes a string of the time in ISO8601 (UTC) format,
#   and returns a python DateTime object.
# Note: returned value is your local time zone.
#
################################################

def ISO8601ToDateTime(strISOTime):
   return time.gmtime(ISO8601ToTime(strISOTime))


#To test:
Test = "2014-09-27T12:05:06.9876"
print ("The test value is: " + Test)
Ans = ISO8601ToTime(Test)
print ("The answer in seconds is: " + str(Ans))
print ("And a Python datetime object is: " + str(ISO8601ToDateTime(Test)))
Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123