337

How do you convert a Python time.struct_time object into a datetime.datetime object?

I have a library that provides the first one and a second library that wants the second one.

codeforester
  • 39,467
  • 16
  • 112
  • 140
static_rtti
  • 53,760
  • 47
  • 136
  • 192

3 Answers3

440

Use time.mktime() to convert the time tuple (in localtime) into seconds since the Epoch, then use datetime.fromtimestamp() to get the datetime object.

from datetime import datetime
from time import mktime

dt = datetime.fromtimestamp(mktime(struct))
hd1
  • 33,938
  • 5
  • 80
  • 91
R Hyde
  • 10,301
  • 1
  • 32
  • 28
  • 71
    Note that this fails before 1900. You modern people never remember this limitation! – mlissner Feb 18 '12 at 07:40
  • 3
    will this lose the `tm_isdst` data? I think so, the resulting datetime object remains naive to the extent to return `None` on `.dst()` even if `struct.tm_isdst` is `1`. – n611x007 May 08 '14 at 14:43
  • 4
    This will usually work. However, it will fail if the time tuple is beyond the values mktime accepts, for example for the value (1970, 1, 1, 0, 0, 0, 0, 1, -1). I have encountered this after parsing the Date header on an HTTP request which returned this tuple. – user3820547 Jul 09 '14 at 14:20
  • As @naxa hints, this will give you a datetime with DST correction, even if the original had no DST offset. – richvdh Sep 10 '14 at 21:33
  • 3
    @richvdh: C standard specifies that `mktime()` should take `tm_isdst` into account and Python `time.mktime()` calls C `mktime()` function on CPython. `mktime()` may choose the wrong local time when it is ambiguous (e.g., during end-of-DST ("fall back") transition) if `struct.tm_isdst` is `-1` or if `mktime()` on the given platform ignores the *input* `tm_isdst`. Also, if the local timezone had different utc offset in the past and C `mktime()` does not use a historical tz database that can provide the old utc offset values then `mktime()` may return a wrong (e.g., by an hour) value too. – jfs Jan 16 '15 at 17:25
  • 1
    @naxa: if `mktime()` doesn't ignore `tm_isdst` on the given platform (it does on mine) then `fromtimestamp()` definitely looses the info: the returned *naive* `datetime` object representing local time may be ambiguous (timestamp -> local time is deterministic (if we ignore leap seconds) but `local time -> timestamp may be ambiguous e.g., during end-of-DST transition). Also, `fromtimestamp()` may choose a wrong utc offset if the it doesn't use a historical tz database. – jfs Jan 16 '15 at 17:34
  • 1
    @J.F.Sebastian: the specific problem I was referring to is that a non-DST struct_time in the summer is always converted to a DST datetime by this method: `datetime.fromtimestamp(mktime((2015,6,1,0,0,0,0,0,0)))` -> `datetime.datetime(2015, 6, 1, 1, 0)`. This may be what you want, but it caused me problems. You're right in that there are further problems around the spec of `mktime()` and how historical tz data is handled. Really this all goes to show that (as pointed out by @lysdexia), datetime conversion is complicated and you need to think carefully about it. – richvdh Jan 26 '15 at 17:48
  • @richvdh: incorrect. `mktime()` on my platform *always* returns the correct result (including DST/non-DST) if input time struct is not ambiguous (or non-existent) i.e., it may be wrong only *during* DST transitions (it ignores input `tm_isdst`) (that is why `pytz.timezone.localize()` method has `is_dst` parameter: to allow disambiguation) or around a positive leap second insertion. I get `datetime(2015,6,1,0,0)` (not `datetime(2015,6,1,1,0)`) on my machine (there is no DST transition at that time in my local timezone). – jfs Jan 27 '15 at 04:33
  • @J.F.Sebastian Not incorrect. I didn't say anything returned the wrong result; merely that there can be an implicit conversion from non-DST to DST-corrected times. 2015/06/01 isn't a DST transition in my timezone either; it is in the middle of the DST period, which is why `datetime.fromtimestamp()` converts it to DST-corrected time. – richvdh Jan 27 '15 at 13:45
  • @richvdh: I can reproduce 1am result with `America/New_York` timezone. It is incorrect to pass `tm_isdst=0` for that day (`2015-06-01`) in that timezone. If you pass the expected for that date `1` or `-1` (to allow `mktime()` to figure out by itself the correct `tm_isdst` value) then `mktime()` returns the correct timestamp. I was wrong: `mktime()` may also return a wrong result given a wrong input even if there is no DST transition at that date. `fromtimestamp()` works correctly for that date on my system. – jfs Jan 27 '15 at 14:33
  • "OverflowError: mktime argument out of range" when I try to call mktime on my struct_time – Csaba Toth Jan 25 '16 at 22:30
  • @mlissner What is the solution prior to the year 1900? – tommy.carstensen Aug 22 '18 at 11:39
  • 1
    Careful, this will convert to your specific timezone – Ferran Apr 15 '20 at 16:19
  • In many cases if the `time_struct` was produced by something like a `time.strptime` call, you can bypass the conversion by simply using `datetime.strptime` instead. That'll put your date in the desired format as a `datetime.datetime` object. – Guillochon Jun 19 '22 at 16:33
145

Like this:

import time, datetime
st = time.localtime()

dt = datetime.datetime(*st[:6])

print(dt)

which prints 2009-11-08 20:32:35.

Update: if you want to include any timezone information from the struct_time value, you could use this method:

def datetime_of_struct_time(st: time.struct_time) -> datetime.datetime:
    "Convert a struct_time to datetime maintaining timezone information when present"
    tz = None
    if st.tm_gmtoff is not None:
        tz = datetime.timezone(datetime.timedelta(seconds=st.tm_gmtoff))
    # datetime doesn't like leap seconds so just truncate to 59 seconds
    if st.tm_sec in {60, 61}:
        return datetime.datetime(*st[:5], 59, tzinfo=tz)
    return datetime.datetime(*st[:6], tzinfo=tz)

Which could be used with the struct_time value from above:

print(datetime_of_struct_time(st))

printing 2009-11-08 20:32:35-04:00.

Sam Mason
  • 15,216
  • 1
  • 41
  • 60
Nadia Alramli
  • 111,714
  • 37
  • 173
  • 152
  • 3
    Don't forget to #import time, datetime – jhwist Nov 08 '09 at 21:00
  • 9
    @jhwist - some things people can be trusted to figure it out on their own :) – orip Nov 08 '09 at 21:10
  • 1
    what does * in front of structTime do? – rodling Dec 07 '12 at 18:44
  • 16
    @rodling the `*` and `**` syntax allows you to expand a listy or dicty type object in to separate arguments - it's one of my favourite pieces of Python lovelyness. See http://docs.python.org/2/tutorial/controlflow.html#unpacking-argument-lists for more info – OrganicPanda Jan 17 '13 at 16:02
  • 2
    @jhwist importing `time` isn't needed if you're already given a `time.struct_time` object. – Nick T Jan 13 '14 at 03:18
  • 11
    Just remember this will give you a ValueError if the struct_time has a leapsecond, eg: `t=time.strptime("30 Jun 1997 22:59:60", "%d %b %Y %H:%M:%S"); datetime.datetime(*t[:6])` – berdario Jan 20 '14 at 00:17
  • 7
    @berdario: to return values compatible with `datetime`: `datetime(*t[:5]+(min(t[5], 59),))` e.g., to accept `"2015-06-30 16:59:60 PDT"`. – jfs Jan 16 '15 at 17:49
  • 1
    Note this also doesn't handle timezones. – mlissner Apr 25 '18 at 20:35
  • 1
    @mlissner have added a variant that includes timezone information – Sam Mason Jul 24 '23 at 13:54
  • @berdario my variant won't give a `ValueError` for leap seconds, but still allows a exception to be thrown for invalid second values. e.g. it's possible to construct a`struct_time` value includes `tm_sec=67` which seems useful to fail on – Sam Mason Jul 24 '23 at 13:59
44

This is not a direct answer to your question (which was answered pretty well already). However, having had times bite me on the fundament several times, I cannot stress enough that it would behoove you to look closely at what your time.struct_time object is providing, vs. what other time fields may have.

Assuming you have both a time.struct_time object, and some other date/time string, compare the two, and be sure you are not losing data and inadvertently creating a naive datetime object, when you can do otherwise.

For example, the excellent feedparser module will return a "published" field and may return a time.struct_time object in its "published_parsed" field:

time.struct_time(
    tm_year=2013, tm_mon=9, tm_mday=9, 
    tm_hour=23, tm_min=57, tm_sec=42, 
    tm_wday=0, tm_yday=252, tm_isdst=0,
)

Now note what you actually get with the "published" field.

Mon, 09 Sep 2013 19:57:42 -0400

By Stallman's Beard! Timezone information!

In this case, the lazy man might want to use the excellent dateutil module to keep the timezone information:

from dateutil import parser
dt = parser.parse(entry["published"])
print "published", entry["published"])
print "dt", dt
print "utcoffset", dt.utcoffset()
print "tzinfo", dt.tzinfo
print "dst", dt.dst()

which gives us:

published Mon, 09 Sep 2013 19:57:42 -0400
dt 2013-09-09 19:57:42-04:00
utcoffset -1 day, 20:00:00
tzinfo tzoffset(None, -14400)
dst 0:00:00

One could then use the timezone-aware datetime object to normalize all time to UTC or whatever you think is awesome.

Gringo Suave
  • 29,931
  • 6
  • 88
  • 75
lysdexia
  • 1,786
  • 18
  • 29
  • 9
    All the `*_parsed` fields from feedparsed are already normalized to UTC as can be checked in [the date parsing documentation](https://pythonhosted.org/feedparser/date-parsing.html?highlight=utc#supporting-additional-date-formats) so this is redundant. – itorres Jun 24 '16 at 22:44
  • 2
    @itorres: If I understand it, this answer is not about normalizing to UTC, but about keeping the timezone information in a `datetime` object which is lost when `feedparser` parses raw string dates. – David Aug 26 '19 at 15:17
  • cant you do this without using a third party library? – PirateApp Aug 04 '22 at 04:50