27

I have a timestamp that is supposed to be in EST:

2014-10-06T18:06:40-04:56

I understand this first part: 2014-10-06T18:06:40, but not -04:56.

What does -04:56 mean here?`

Here is how I got that timestamp:

import datetime
start_time = datetime.datetime(year  = 2014, 
                               month = 10, 
                               day   = 6, 
                               hour  = 18, 
                               tzinfo = pytz.timezone('US/Eastern'))
end_time   = start_time + datetime.timedelta(seconds=400)

And then:

end_time.isoformat()

returns:

2014-10-06T18:06:40-04:56
Josh
  • 11,979
  • 17
  • 60
  • 96
  • First, EST is UTC-5, not UTC-4. – abarnert Oct 08 '14 at 19:46
  • Second, when I imbue a `datetime` object with an EST timezone, whether from `pytz` or `tzlocal` or anything else I can find, I get `-05:00` as expected. Of course I can always go out of my way to construct timezone that's at `-04:56` and name it EST if I want, and nothing in Python or any module I have will notice that I'm going out of my way to confuse myself, but… it's not exactly easy to do. – abarnert Oct 08 '14 at 19:47
  • Josh, it means what you think it means. It describes a timezone that lags UTC by four hours and fifty-six minutes. That timestamp is equivalent to `2014-10-06T23:02:40Z`. Where did you get that timestamp? Or, what software generated that timestamp? – Robᵩ Oct 08 '14 at 19:53
  • @Robᵩ Sorry for the delay - I updated the post – Josh Oct 08 '14 at 20:17
  • @abarnert I just updated the OP. – Josh Oct 08 '14 at 20:19
  • Please create a short, complete program that demonstrates the error and copy-paste that program into your question. – Robᵩ Oct 08 '14 at 20:23
  • @Robᵩ What is missing from the example? Are you not able to get the same string? – Josh Oct 08 '14 at 20:47
  • 1
    @Josh: For future reference, it's generally better to include either a complete script that can be run, or if not that a complete interactive-prompt transcript, including the imports and everything. It's not really ambiguous or complicated in this case, but it never hurts to be thorough, especially when editing a question that was initially missing information. – abarnert Oct 08 '14 at 21:56
  • +1 Thanks @abarnert I agree. Will certainly try to do so the next time. – Josh Oct 08 '14 at 21:59

3 Answers3

40

The problem is that pytz:

… differs from the documented Python API for tzinfo implementations; if you want to create local wallclock times you need to use the localize() method documented in this document …

Further down, it says:

Unfortunately using the tzinfo argument of the standard datetime constructors "does not work" with pytz for many timezones.

>>> datetime(2002, 10, 27, 12, 0, 0, tzinfo=amsterdam).strftime(fmt)
'2002-10-27 12:00:00 LMT+0020'

So, you need to do what the docs suggest—using normalize, constructing UTC times and using astimezone, etc. Which one you want depends on exactly what you're trying to do. For example:

>>> from datetime import datetime
>>> from pytz import timezone
>>> utc = timezone('UTC')
>>> eastern = timezone('US/Eastern')
>>> datetime(2014, 10, 6, 18, tzinfo=eastern).isoformat()
'2014-10-06T18:00:00-04:56'
>>> eastern.normalize(datetime(2014, 10, 6, 18, tzinfo=eastern)).isoformat()
'2014-10-06T18:56:00-04:00'
>>> datetime(2014, 10, 6, 18, tzinfo=utc).astimezone(eastern).isoformat()
'2014-10-06T14:00:00-04:00'
>>> eastern.localize(datetime(2014, 10, 6, 18)).isoformat()
'2014-10-06T18:00:00-04:00'

I think it's the last you want. As the docs for localize say:

Convert naive time to local time.

This method should be used to construct localtimes, rather than passing a tzinfo argument to a datetime constructor.

And I think constructing a local time is exactly what you wanted here.


If you're wondering why… well, if you look at the data that's in your Olson database, or just print out eastern._utcoffset, you'll see -1 day, +68640 minutes. That's 19.0166+ hours, not 19. Why? Because every timezone is defined with its starting offset, with adjustments from there. Eastern is based on New York's timezone as of 1883 Nov 18 12:03:58, at which point it was -04:56:02 from GMT. There's an adjustment for dates starting in 1920 that subtracts the extra 00:03:58. And of course the yearly adjustments back and forth one hour for DST. So, as of right now, Eastern is -04:00, but without any idea of what date it's supposed to represent, it's -04:56. And, because datetime just asks a timezone for its offset, rather than its offset at a particular time, that's what it gets.


One last thing: EST is Eastern Standard Time, which is -05:00. That's not the time zone of any location in the US on 6 October 2014, because in 2014, the US's daylight saving time goes to 2 November. (There used to be counties in Indiana that were on EST during the summer, but there no longer are.) What you're looking for is EDT, Eastern Daylight Time, which is -04:00. Or, of course, ET, which is EDT during the summer and EST during the winter, which is what you get from looking up 'US/Eastern' or 'America/New_York'.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • 3
    Wow, this bit me hard. – Josh Oct 08 '14 at 21:27
  • Where does the method `timezone` come from? – Josh Oct 08 '14 at 21:29
  • @Josh: Sorry, let me add the imports. It comes from `pytz`. – abarnert Oct 08 '14 at 21:30
  • @Josh: Anyway, Every time I have to do timezone-related stuff after more than a few months away, I run into this again, so as soon as I saw your edited question with `pytz` I knew what was happening (although this time it was a discussion about adding `pytz` or something like it to the stdlib, rather than using it myself, that reminded me). – abarnert Oct 08 '14 at 21:31
  • 1
    Thanks! One quick note: the last command does not return the string that you posted (at least on my machine). I get: `'2014-10-06T14:00:00-04:00'`. So I guess that that route does not work (you have to pre-calculate the time difference..) – Amelio Vazquez-Reina Oct 08 '14 at 21:34
  • Actually, both of them are giving me `2014-10-06T14:00:00-04:00` (??) – Amelio Vazquez-Reina Oct 08 '14 at 21:36
  • I am in `Python 3.4.1`, could that explain it? – Amelio Vazquez-Reina Oct 08 '14 at 21:38
  • @user815423426: Sorry, that's what I get for doing everything in Pacific and then editing it manually to Eastern, instead of just running the code directly… I'll edit it. – abarnert Oct 08 '14 at 21:42
  • Thanks again! So of the last two statements, which one returns the time that we really want? (`18:00 EST on October 6, 2014`). The last one? It seems to be off by 4 minutes (even after considering your addendum at the end of the post) – Josh Oct 08 '14 at 21:46
  • 1
    @Josh: First, as I mentioned before, there is no EST on October 6, 2014, only EDT; DST goes until 2 November in the US. But anyway, the easiest way to get what you actually want here is to construct a naive datetime and then `localize` it; see my new fourth example. – abarnert Oct 08 '14 at 21:49
  • you should indicate more clearly which code is broken. I've posted a separate answer to [show the right way without mixing it with the incorrect code](http://stackoverflow.com/a/26328488/4279) – jfs Oct 12 '14 at 18:31
7

What does -04:56 mean here?`

It means that the code that generates the input timestamp is broken in such a way that demonstrates the lack of understanding of how pytz timezones work. You should not trust its results, if you think that only the UTC offset (-04:56) is wrong but the date itself is the correct time in Eastern Time Zone then to parse the time, do:

#!/usr/bin/env python
from datetime import datetime, timedelta
import pytz

tz = pytz.timezone('America/New_York')

naive = datetime.strptime("2014-10-06T18:06:40-04:56"[:-6],
                          "%Y-%m-%dT%H:%M:%S")
start_time = tz.localize(naive, is_dst=None)
end_time = tz.normalize(start_time + timedelta(seconds=400))
print(start_time.isoformat())
print(end_time.isoformat())
  • you should use tz.localize() instead of assigning tzinfo attribute directly
  • is_dst=None asserts that the input time exists and it is unambiguous
  • tz.normalize() is necessary if date arithmetics crosses DST boundaries

Output

2014-10-06T18:06:40-04:00
2014-10-06T18:13:20-04:00

Why you need localize(), normalize() is described in the pytz docs (the part about localize()/normalize() is the very first note in the documentation).

Why -04:56 UTC offset is wrong in 2014 in Eastern Time Zone

UTC offset in the same place may be different at different times due to DST transitions or other reasons (such as war or because some politician thinks it is a good idea) e.g., here's possible values for US/Eastern:

>>> import pytz
>>> pytz.timezone('US/Eastern')
{(datetime.timedelta(-1, 72000),
  datetime.timedelta(0, 3600),
  'EWT'): <DstTzInfo 'US/Eastern' EWT-1 day, 20:00:00 DST>,
 (datetime.timedelta(-1, 68640),
  datetime.timedelta(0),
  'LMT'): <DstTzInfo 'US/Eastern' LMT-1 day, 19:04:00 STD>,
 (datetime.timedelta(-1, 72000),
  datetime.timedelta(0, 3600),
  'EDT'): <DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>,
 (datetime.timedelta(-1, 68400),
  datetime.timedelta(0),
  'EST'): <DstTzInfo 'US/Eastern' EST-1 day, 19:00:00 STD>,
 (datetime.timedelta(-1, 72000),
  datetime.timedelta(0, 3600),
  'EPT'): <DstTzInfo 'US/Eastern' EPT-1 day, 20:00:00 DST>}

Notice the UTC offset for LMT tzinfo: timedelta(-1, 68640) == '-4:56:00'. The way to get it is to use the incorrect code:

#XXX BROKEN, DO NOT DO IT
>>> dt = datetime(2014, 10, 6, tzinfo=pytz.timezone('US/Eastern'))
>>> dt
datetime.datetime(2014, 10, 6, 0, 0, tzinfo=<DstTzInfo 'US/Eastern' LMT-1 day, 19:04:00 STD>)
>>> dt.isoformat()
2014-10-06T00:00:00-04:56

Assigning tzinfo directly doesn't allow pytz to choose the correct tzinfo for the given time and some random tzinfo object among available is used instead. You should always use tz.localize() to attach the correct timezone info.

jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • I don't think it uses some random object among the available; it uses the defining offset for the timezone, as specified in the Olson database, which is almost always the offset at the time the timezone was first established. Eastern was defined retroactively as local mean time at… I forget which location… prior to 1920, so that's the definition Olson uses; the -05:00 is listed as a 4-minute change starting in 1920. – abarnert Oct 13 '14 at 08:10
  • It is random in a sense that you can't rely on it. It doesn't matter how the implementation chooses the timezone as long as it is not guaranteed in the docs. – jfs Oct 13 '14 at 08:16
1

I suggest looking at arrow. The above answers worked but make the code really confusing to follow. Arrow lets you do this simply using:

In [82]: start_time = arrow.get(2014, 10, 6, 18).to('US/Eastern')
In [83]: end_time = start_time.shift(seconds=400)
In [84]: start_time
Out[84]: <Arrow [2014-10-06T14:00:00-04:00]>
In [85]: end_time
Out[85]: <Arrow [2014-10-06T14:06:40-04:00]>

You can retrieve the datetime objects by using

In [86]: start_time.datetime
Out[86]: datetime.datetime(2014, 10, 6, 14, 0, tzinfo=tzfile('/usr/share/zoneinfo/US/Eastern'))
frmdstryr
  • 20,142
  • 3
  • 38
  • 32