TL;DR: You should always store datetimes in UTC and convert to proper timezone on display.
A timezone offset refers to how many hours the timezone is from Coordinated Universal Time (UTC). The offset of UTC is +00:00
, and the offset of Asia/Taipei
timezone is UTC+08:00
(you could also present it as GMT+08:00
). Basically, there is no perceptible difference between Greenwich Mean Time (GMT) and UTC.
The local time subtracts the offset of its timezone is UTC time. For instance, 18:00+08:00
of Asia/Taipei
minuses timezone offset +08:00
is 10:00+00:00
, 10 o'clock of UTC. On the other hand, UTC time pluses local timezone offset is local time.
ref:
https://opensource.com/article/17/5/understanding-datetime-python-primer
https://julien.danjou.info/blog/2015/python-and-timezones
到底是 GMT+8 還是 UTC+8?
http://pansci.asia/archives/84978
Installation
$ pip install -U python-dateutil pytz tzlocal
Show System Timezone
import tzlocal
tzlocal.get_localzone()
# <DstTzInfo 'Asia/Taipei' LMT+8:06:00 STD>
tzlocal.get_localzone().zone
# 'Asia/Taipei'
from time import gmtime, strftime
print(strftime("%z", gmtime()))
# +0800
ref:
https://github.com/regebro/tzlocal
https://stackoverflow.com/questions/13218506/how-to-get-system-timezone-setting-and-pass-it-to-pytz-timezone/
Find Timezones Of A Certain Country
import pytz
pytz.country_timezones('tw')
# ['Asia/Taipei']
pytz.country_timezones('cn')
# ['Asia/Shanghai', 'Asia/Urumqi']
ref:
https://pythonhosted.org/pytz/#country-information
Offset-naive Datetime
Any naive datetime
would be present as local timezone but without tzinfo
, so it is buggy.
A naive datetime
object contains no timezone information. The datetime_obj.tzinfo
will be set to None
if the object is naive. Actually, datetime
objects without timezone should be considered as a "bug" in your application. It is up for the programmer to keep track of which timezone users are working in.
import datetime
import dateutil.parser
datetime.datetime.now()
# return the current date and time in local timezone, in this example: Asia/Taipei (UTC+08:00)
# datetime.datetime(2018, 2, 2, 9, 15, 6, 211358)), naive
datetime.datetime.utcnow()
# return the current date and time in UTC
# datetime.datetime(2018, 2, 2, 1, 15, 6, 211358), naive
dateutil.parser.parse('2018-02-04T16:30:00')
# datetime.datetime(2018, 2, 4, 16, 30), naive
ref:
https://docs.python.org/3/library/datetime.html
https://dateutil.readthedocs.io/en/stable/
Offset-aware Datetime
A aware datetime
object embeds a timezone information. Rules of thumb for timezone in Python:
- Always work with "offset-aware"
datetime
objects. - Always store datetime in UTC and do timezone conversion only when interacting with users.
- Always use ISO 8601 as input and output string format.
There are two useful methods: pytz.utc.localize(naive_dt)
for converting naive datetime
to timezone be offset-aware, and aware_dt.astimezone(pytz.timezone('Asia/Taipei'))
for adjusting timezones of offset-aware objects.
You should avoid naive_dt.astimezone(some_tzinfo)
which would be converted to aware datetime as system timezone then convert to some_tzinfo
timezone.
import datetime
import pytz
now_utc = pytz.utc.localize(datetime.datetime.utcnow())
# equals to datetime.datetime.now(pytz.utc)
# equals to datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
# datetime.datetime(2018, 2, 4, 10, 17, 40, 679562, tzinfo=<UTC>), aware
now_taipei = now_utc.astimezone(pytz.timezone('Asia/Taipei'))
# convert to another timezone
# datetime.datetime(2018, 2, 4, 18, 17, 40, 679562, tzinfo=<DstTzInfo 'Asia/Taipei' CST+8:00:00 STD>), aware
now_utc.isoformat()
# '2018-02-04T10:17:40.679562+00:00'
now_taipei.isoformat()
# '2018-02-04T18:17:40.679562+08:00'
now_utc == now_taipei
# True
For working with pytz
, it is recommended to call tz.localize(naive_dt)
instead of naive_dt.replace(tzinfo=tz)
. dt.replace(tzinfo=tz)
does not handle daylight savings time correctly.
dt1 = datetime.datetime.now(pytz.timezone('Asia/Taipei'))
# datetime.datetime(2018, 2, 4, 18, 22, 28, 409332, tzinfo=<DstTzInfo 'Asia/Taipei' CST+8:00:00 STD>), aware
dt2 = datetime.datetime(2018, 2, 4, 18, 22, 28, 409332, tzinfo=pytz.timezone('Asia/Taipei'))
# datetime.datetime(2018, 2, 4, 18, 22, 28, 409332, tzinfo=<DstTzInfo 'Asia/Taipei' LMT+8:06:00 STD>), aware
dt1 == dt2
# False
ref:
https://pythonhosted.org/pytz/
Naive and aware datetime
objects are not comparable.
naive = datetime.datetime.utcnow()
aware = pytz.utc.localize(naive)
naive == aware
# False
naive >= aware
# TypeError: can't compare offset-naive and offset-aware datetimes
Parse String to Datetime
python-dateutil
usually comes in handy.
import dateutil.parser
import dateutil.tz
dt1 = dateutil.parser.parse('2018-02-04T19:30:00+08:00')
# datetime.datetime(2018, 2, 4, 19, 30, tzinfo=tzoffset(None, 28800)), aware
dt2 = dateutil.parser.parse('2018-02-04T11:30:00+00:00')
# datetime.datetime(2018, 2, 4, 11, 30, tzinfo=tzutc()), aware
dt3 = dateutil.parser.parse('2018-02-04T11:30:00Z')
# datetime.datetime(2018, 2, 4, 11, 30, tzinfo=tzutc()), aware
dt1 == dt2 == dt3
# True
ref:
https://dateutil.readthedocs.io/en/stable/
Convert Datetime To Unix Timestamp
import datetime
naive_dt = datetime.datetime(2018, 9, 10, 0, 0, 0)
naive_timestamp = aware_dt.timestamp()
# naive_dt would be in local timezone, in this example: Asia/Taipei (UTC+08:00)
aware_dt = datetime.datetime(2018, 9, 10, 0, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(hours=8)))
aware_timestamp = aware_dt.timestamp()
naive_timestamp == aware_timestamp
# True
# MongoDB stores all datetimes in UTC timezone
dt_fetched_from_mongodb.replace(tzinfo=datetime.timezone.utc).timestamp()
Parse Unix Timestamp To Datetime
import datetime
import time
import pytz
ts = time.time()
# seconds since the Epoch (1970-01-01T00:00:00 in UTC)
# 1517748706.063205
dt1 = datetime.datetime.fromtimestamp(ts)
# return the date and time of the timestamp in local timezone, in this example: Asia/Taipei (UTC+08:00)
# datetime.datetime(2018, 2, 4, 20, 51, 46, 63205), naive
dt2 = datetime.datetime.utcfromtimestamp(ts)
# return the date and time of the timestamp in UTC timezone
# datetime.datetime(2018, 2, 4, 12, 51, 46, 63205), naive
pytz.timezone('Asia/Taipei').localize(dt1) == pytz.utc.localize(dt2)
# True
We might receive an Unix timestamp from a JavaScript client.
var moment = require('moment')
var ts = moment('2018-02-02').unix()
// 1517500800
ref:
https://momentjs.com/docs/#/parsing/unix-timestamp/
Store Datetime In Databases
- MySQL lets developers decide what timezone should be used, and you should convert datetime to UTC before saving into database.
- MongoDB assumes that all the timestamp are in UTC, and you have to normalize datetime to UTC.
ref:
https://tommikaikkonen.github.io/timezones/
https://blog.elsdoerfer.name/2008/03/03/fun-with-timezones-in-django-mysql/
Tools
ref:
https://www.epochconverter.com/
https://www.timeanddate.com/worldclock/converter.html