anon_dcs3spp
anon_dcs3spp

Reputation: 3022

Python astimezone 1 hour behind expected result compared to adding timedelta to UTC date

I am trying to do some testing with time offsets from UTC. I have tried two methods as shown in the code listing below:

  1. Adding timedelta to UTC datetime
  2. Using astimezone method on UTC datetime

In the short test program below the resultant date from astimezone method is 1 hour behind. I do not understand why??

from datetime import datetime, timedelta, timezone

if __name__ == "__main__":
    utc_now = datetime.utcnow()
    target_time = int((utc_now + timedelta(hours=10)).timestamp())
    timestamp = utc_now.astimezone(timezone(timedelta(hours=10)))

    print(f"datetime.utcnow() = {utc_now.isoformat()}")
    print(
        f"datetime.utcnow() + 10 hours using timedelta = {datetime.fromtimestamp(target_time).isoformat()}"
    )
    print(f"datetime.utcnow() + 10 hours using astimezone = {timestamp.isoformat()}")
datetime.utcnow() = 2021-09-04T16:12:53.753059
datetime.utcnow() + 10 hours using timedelta = 2021-09-05T02:12:53
datetime.utcnow() + 10 hours using astimezone = 2021-09-05T01:12:53.753059+10:00

Edit - Update - datetime.now (timezone.utc )

from datetime import datetime, timedelta, timezone

if __name__ == "__main__":
    utc_now = datetime.now(timezone.utc)

    target_time = int((utc_now + timedelta(hours=10)).timestamp())
    timestamp = utc_now.astimezone(timezone(timedelta(hours=10)))

    print(f"datetime.utcnow() = {utc_now.isoformat()}")
    print(
        "datetime.utcnow() + 10 hours using timedelta ="
        f" {datetime.fromtimestamp(target_time).isoformat()}"
    )
    print(f"datetime.utcnow() + 10 hours using astimezone = {timestamp.isoformat()}")

Tried to make timezone aware UTC datetime now with the same result.

How do I use astimezone to get an equivalent result?

Upvotes: 1

Views: 1225

Answers (2)

FObersteiner
FObersteiner

Reputation: 25594

The problem here is the assumption that datetime.utcnow() gives you UTC. It does not. It gives you a naive datetime object that Python still treats as local time although hours, minutes etc. resemble UTC.

Set tzinfo adequately to get consistent results (don't use utcnow at all if possible! - unless you know what you're doing):

from datetime import datetime, timedelta, timezone

utc_now = datetime(2021, 9, 4, tzinfo=timezone.utc)
target_time = int((utc_now + timedelta(hours=10)).timestamp())
timestamp = utc_now.astimezone(timezone(timedelta(hours=10)))

print(f"utc_now = {utc_now.isoformat()}")
print(f"utc_now + 10 hours using timedelta = {datetime.fromtimestamp(target_time, tz=timezone.utc).isoformat()}")
print(f"utc_now + 10 hours using astimezone = {timestamp.isoformat()}")

# utc_now = 2021-09-04T00:00:00+00:00
# utc_now + 10 hours using timedelta = 2021-09-04T10:00:00+00:00
# utc_now + 10 hours using astimezone = 2021-09-04T10:00:00+10:00

The key here is understanding the difference between naive datetime (=local time by default) and aware datetime (the time zone you set...). For more time zone handling, see also the zoneinfo lib.

Upvotes: 1

FObersteiner
FObersteiner

Reputation: 25594

an aspect that is also relevant here in the context of working with time zones in Python: timedelta arithmetic is wall time arithmetic - i.e. what you would observe on a wall clock that gets adjusted to DST active/inactive. In the following example, I add 24 hours:

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

t0 = datetime(2021, 10, 30, 20, tzinfo=ZoneInfo("Europe/Berlin")) # DST active
t1 = t0 + timedelta(hours=24) # add 24 hours -> DST is inactive
print(t0, t1)
# 2021-10-30 20:00:00+02:00 2021-10-31 20:00:00+01:00
print((t1-t0).total_seconds()/3600)
# 24.0

Now watch how they become 25 hours - not because of magic but because of the wall clock would show local time, not UTC...

t0_utc, t1_utc = t0.astimezone(ZoneInfo("UTC")), t1.astimezone(ZoneInfo("UTC"))
print(t0_utc, t1_utc)
# 2021-10-30 18:00:00+00:00 2021-10-31 19:00:00+00:00
print((t1_utc-t0_utc).total_seconds()/3600)
# 25.0

Note: since Europe/Berlin is the time zone my OS is configured to use, this would also work with t0 and t1 being naive datetime objects!

Upvotes: 1

Related Questions