Search code examples
pythondatetimeutc

python: utcfromtimestamp vs fromtimestamp, when the timestamp is based on utcnow()


Pretty sure it's an easy one but I don't get it.

My local TZ is currently GMT+3, and when I take timestamp from datetime.utcnow().timestamp() it is indeed giving me 3 hours less than datetime.now().timestamp()

During another process in my flow, I take that utc timestamp and need to turn it into datetime.

When I'm doing fromtimestamp I get the right utc hour, but when I'm using utcfromtimestamp I get another 3 hours offset.

The documentation, though, asks me to use fromtimestamp for local timezone, and utcfromtimestamp for utc usages.

What am I missing ? is the initial assumption for both funcs is that the timestamp is given in local timezone ?

Thank you :)


Solution

  • The key thing to notice when working with datetime objects and their POSIX timestamps (Unix time) at the same time is that naive datetime objects (the ones without time zone information) are assumed by Python to refer to local time (OS setting). In contrast, a POSIX timestamp (should) always refer to seconds since the epoch UTC. You can unambiguously obtain that e.g. from time.time(). In your example, not-so-obvious things happen:

    1. datetime.now().timestamp() - now() gives you a naive datetime object that resembles local time. If you call for the timestamp(), Python converts the datetime to UTC and calculates the timestamp for that.

    2. datetime.utcnow().timestamp() - utcnow() gives you a naive datetime object that resembles UTC. However, if you call timestamp(), Python assumes (since naive) that the datetime is local time - and converts to UTC again before calculating the timestamp! The resulting timestamp is therefore off from UTC by twice your local time's UTC offset.

    A code example. Let's make some timestamps. Note that I'm on UTC+2 (CEST), so offset is -7200 s.

    import time
    from datetime import datetime, timezone
    
    ts_ref = time.time() # reference POSIX timestamp
    
    ts_utcnow = datetime.utcnow().timestamp() # dt obj UTC but naive - so also assumed local
    
    ts_now = datetime.now().timestamp() # dt obj naive, assumed local
    
    ts_loc_utc = datetime.now(tz=timezone.utc).timestamp() # dt obj localized to UTC
    
    print(int(ts_utcnow - ts_ref))
    # -7200 # -> ts_utcnow doesn't refer to UTC!
    print(int(ts_now - ts_ref))
    # 0 # -> correct
    print(int(ts_loc_utc - ts_ref))
    # 0 # -> correct
    

    I hope this clarifies that if you call datetime.utcfromtimestamp(ts_utcnow), you get double the local time's UTC offset. Python assumes (which I think is pretty sane) that the timestamp refers to UTC - which in fact, it does not.

    My suggestion would be to use timezone-aware datetime objects; like datetime.now(tz=timezone.utc). If you're working with time zones, the dateutil library or Python 3.9's zoneinfo module are very helpful. And if you want to dig deep, have a look at the datetime src code.