8.10. Datetime Timezone

  • Always keep dates and times only in UTC (important!)

  • Datetimes should be converted to local time only when displaying to user

  • Computerphile Time & Time Zones [3]

  • Abolition of Time Zones [1]

  • Since Python 3.9: PEP 615 -- Support for the IANA Time Zone Database in the Standard Library

  • pip install tzdata

8.10.1. SetUp

>>> from datetime import date, time, datetime, timedelta, timezone
>>> from zoneinfo import ZoneInfo

8.10.2. How Many Timezones Are There?

  • 24 standard time zones (UTC-1200 to UTC+1200)

  • 38 time zones with 30 minutes offset (India UTC+0530, Australia UTC+0930)

  • 8 time zones with 45 minutes offset (Nepal UTC+0545)

  • Total 70 time zones

  • Some countries has multiple time zones (USA 6, Russia 11)

  • TZ database contains 598 time zones (341 zones + 257 links) [4]

  • Some time zones has changed multiple times in history

  • Some time zones has different Daylight Saving Time rules in history

  • Some time zones has changed UTC offset multiple times in history

../../_images/datetime-timezones-1.png
../../_images/datetime-timezone-tzdata.png

Figure 8.1. The tz database partitions the world into regions where local clocks all show the same time. This map was made by combining version 2023d of the List of tz database time zones with OpenStreetMap data, using open source software [4]

8.10.3. Daylight Saving Time

  • Daylight Saving Time date is different for each country and even US state

  • Australia is 9h 30m shifted

  • India is UTC+0530 shifted

  • Nepal is UTC+0545 shifted

  • In southern hemisphere the Daylight Saving Time is opposite direction

  • They subtract hour in March and add in October

  • Samoa is on the international date line

  • Samoa changed from UTC-1200 to UTC+1200 for easier trades with Australia

  • During World War II England was GMT+0200

  • Libya in 2013 discontinued DST with couple of days notice

  • Israel is on a different timezone than Palestine (multiple timezones in one location, based on nationality)

  • Change from Julian to Gregorian calendar caused to skip few weeks

  • In 18th century World change from Julian to Gregorian calendar

  • In 20th century Russia change from Julian to Gregorian calendar (different days which was skipped than for worldwide change)

  • In britain until 16th century the year started on 25th of March

  • Mind leap seconds (add, subtract)

  • UTC includes leap seconds

  • Astronomical time does not include leap seconds

  • Google invented smear second (on the day of leap second) they add a small fraction of a second to each second that day until midnight

  • Not all cities has DST https://www.timeanddate.com/time/us/arizona-no-dst.html

Comparing datetime works only when all has the same timezone (UTC):

../../_images/datetime-compare.png

8.10.4. Timezone Naive Datetimes

>>> datetime(1957, 10, 4, 19, 28, 34)
datetime.datetime(1957, 10, 4, 19, 28, 34)
>>> datetime.now()
datetime.datetime(1957, 10, 4, 19, 28, 34)

8.10.5. Timezone Aware Datetimes

>>> datetime.now(timezone.utc)
datetime.datetime(1957, 10, 4, 19, 28, 34, tzinfo=datetime.timezone.utc)
>>> datetime(1957, 10, 4, 19, 28, 34, tzinfo=timezone.utc)
datetime.datetime(1957, 10, 4, 19, 28, 34, tzinfo=datetime.timezone.utc)
>>> dt = datetime(1957, 10, 4, 19, 28, 34)
>>> dt.replace(tzinfo=timezone.utc)
datetime.datetime(1957, 10, 4, 19, 28, 34, tzinfo=datetime.timezone.utc)

8.10.6. UTCNow

  • datetime.utcnow() produces timezone naive datetimes!

>>> datetime.utcnow()
datetime.datetime(1957, 10, 4, 17, 28, 34)
>>> datetime.utcnow(tz=timezone.utc)
Traceback (most recent call last):
TypeError: datetime.utcnow() takes no keyword arguments
>>> datetime.utcnow(timezone.utc)
Traceback (most recent call last):
TypeError: datetime.utcnow() takes no arguments (1 given)

8.10.7. IANA Time Zone Database

IANA 2017a timezone database [2]:

../../_images/datetime-timezone-iana2017a.png

8.10.8. ZoneInfo

>>> utc = ZoneInfo('UTC')
>>> est = ZoneInfo('US/Eastern')
>>> cet = ZoneInfo('Europe/Warsaw')
>>> alm = ZoneInfo('Asia/Almaty')

Working with ZoneInfo objects:

>>> dt = datetime(1969, 7, 21, 2, 56, 15, tzinfo=ZoneInfo('UTC'))
>>> print(dt)
1969-07-21 02:56:15+00:00
>>> dt += timedelta(days=7)
>>> print(dt)
1969-07-28 02:56:15+00:00

ZoneInfo objects knows Daylight Saving Time:

>>> dt = datetime(2000, 1, 1, tzinfo=ZoneInfo('America/Los_Angeles'))  # Daylight saving time
>>>
>>> dt.tzname()
'PST'
>>>
>>> dt += timedelta(days=100)  # Standard time
>>> dt.tzname()
'PDT'

8.10.9. Use Case - 1

>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>>
>>>
>>> UTC = ZoneInfo('UTC')
>>> BAJKONUR = ZoneInfo('Asia/Almaty')
>>> WAW = ZoneInfo('Europe/Warsaw')
>>> LOS_ANGELES = ZoneInfo('America/Los_Angeles')
>>>
>>>
>>> dt = datetime(1961, 4, 12, 6, 7, tzinfo=UTC)
>>> dt
datetime.datetime(1961, 4, 12, 6, 7, tzinfo=zoneinfo.ZoneInfo(key='UTC'))
>>>
>>> dt.astimezone(BAJKONUR)
datetime.datetime(1961, 4, 12, 12, 7, tzinfo=zoneinfo.ZoneInfo(key='Asia/Almaty'))
>>>
>>> dt.astimezone(WAW)
datetime.datetime(1961, 4, 12, 7, 7, tzinfo=zoneinfo.ZoneInfo(key='Europe/Warsaw'))
>>>
>>> dt.astimezone(LOS_ANGELES)
datetime.datetime(1961, 4, 11, 22, 7, tzinfo=zoneinfo.ZoneInfo(key='America/Los_Angeles'))

8.10.10. Use Case - 2

Descriptor Timezone Converter:

>>> from dataclasses import dataclass
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>>
>>>
>>> class Timezone:
...     def __init__(self, name):
...         self.timezone = ZoneInfo(name)
...
...     def __get__(self, parent, *args):
...         utc = parent.utc.replace(tzinfo=ZoneInfo('UTC'))
...         return utc.astimezone(self.timezone)
...
...     def __set__(self, parent, new_datetime):
...         local_time = new_datetime.replace(tzinfo=self.timezone)
...         parent.utc = local_time.astimezone(ZoneInfo('UTC'))
>>>
>>>
>>> @dataclass
... class Time:
...     utc = datetime.now(tz=ZoneInfo('UTC'))
...     warsaw = Timezone('Europe/Warsaw')
...     eastern = Timezone('America/New_York')
...     pacific = Timezone('America/Los_Angeles')
>>>
>>>
>>> t = Time()
>>>
>>> # Gagarin's launch to space
>>> t.utc = datetime(1961, 4, 12, 6, 7)
>>>
>>> print(t.utc)
1961-04-12 06:07:00
>>> print(t.warsaw)
1961-04-12 07:07:00+01:00
>>> print(t.eastern)
1961-04-12 01:07:00-05:00
>>> print(t.pacific)
1961-04-11 22:07:00-08:00
>>>
>>>
>>> # Armstrong's first Lunar step
>>> t.warsaw = datetime(1969, 7, 21, 3, 56, 15)
>>>
>>> print(t.utc)
1969-07-21 02:56:15+00:00
>>> print(t.warsaw)
1969-07-21 03:56:15+01:00
>>> print(t.eastern)
1969-07-20 22:56:15-04:00
>>> print(t.pacific)
1969-07-20 19:56:15-07:00

8.10.11. References

8.10.12. Assignments

# %% About
# - Name: Datetime Timezone ZoneInfo
# - Difficulty: easy
# - Lines: 7
# - Minutes: 3

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% English
# 0. If you're on Windows then install module `tzdata` (`pip install tzdata`)
# 1. Define variable `result_a: ZoneInfo` with 'UTC' timezone
# 2. Define variable `result_b: ZoneInfo` with 'GMT' timezone
# 3. Define variable `result_c: ZoneInfo` with 'Etc/GMT' timezone
# 4. Define variable `result_d: ZoneInfo` with 'Europe/London' timezone
# 5. Define variable `result_e: ZoneInfo` with 'Greenwich' timezone
# 6. Define variable `result_f: ZoneInfo` with 'Universal' timezone
# 7. Define variable `result_g: ZoneInfo` with 'Zulu' timezone
# 8. Use `zoneinfo`
# 9. Run doctests - all must succeed

# %% Polish
# 0. Jeżeli masz Windows to zainstaluj moduł `tzdata` (`pip install tzdata`)
# 1. Zdefiniuj zmienną `result_a: ZoneInfo` ze strefą czasową 'UTC'
# 2. Zdefiniuj zmienną `result_b: ZoneInfo` ze strefą czasową 'GMT'
# 3. Zdefiniuj zmienną `result_c: ZoneInfo` ze strefą czasową 'Etc/GMT+0'
# 4. Zdefiniuj zmienną `result_d: ZoneInfo` ze strefą czasową 'Europe/London'
# 5. Zdefiniuj zmienną `result_e: ZoneInfo` ze strefą czasową 'Greenwich'
# 6. Zdefiniuj zmienną `result_f: ZoneInfo` ze strefą czasową 'Universal'
# 7. Zdefiniuj zmienną `result_g: ZoneInfo` ze strefą czasową 'Zulu'
# 8. Użyj `zoneinfo`
# 9. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `zoneinfo`
# - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

# %% References
# [1] Wikipedia. List of tz database time zones.
#     Retrieved: 2022-12-01
#     Year: 2022
#     URL: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# [2] IANA. Time Zone Database.
#     Retrieved: 2022-12-01
#     Year: 2022
#     URL: https://data.iana.org/time-zones/releases/

# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from datetime import datetime
>>>
>>> def utcoffset(tz: ZoneInfo):
...     dt = datetime(2000, 1, 1)
...     return tz.utcoffset(dt).total_seconds()


>>> assert result_a is not Ellipsis, \
'Variable `result_a` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_a, ZoneInfo), \
'Variable `result_a` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_a) == 0, \
'Variable `result_a` is invalid time zone, check IANA Time Zone Database'


>>> assert result_b is not Ellipsis, \
'Variable `result_b` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_b, ZoneInfo), \
'Variable `result_b` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_b) == 0, \
'Variable `result_b` is invalid time zone, check IANA Time Zone Database'


>>> assert result_c is not Ellipsis, \
'Variable `result_c` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_c, ZoneInfo), \
'Variable `result_c` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_c) == 0, \
'Variable `result_c` is invalid time zone, check IANA Time Zone Database'


>>> assert result_d is not Ellipsis, \
'Variable `result_d` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_d, ZoneInfo), \
'Variable `result_d` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_d) == 0, \
'Variable `result_d` is invalid time zone, check IANA Time Zone Database'


>>> assert result_e is not Ellipsis, \
'Variable `result_e` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_e, ZoneInfo), \
'Variable `result_e` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_e) == 0, \
'Variable `result_e` is invalid time zone, check IANA Time Zone Database'


>>> assert result_f is not Ellipsis, \
'Variable `result_f` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_f, ZoneInfo), \
'Variable `result_f` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_f) == 0, \
'Variable `result_f` is invalid time zone, check IANA Time Zone Database'


>>> assert result_g is not Ellipsis, \
'Variable `result_g` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_g, ZoneInfo), \
'Variable `result_g` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_g) == 0, \
'Variable `result_g` is invalid time zone, check IANA Time Zone Database'
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -f -v myfile.py`

# %% Imports
from zoneinfo import ZoneInfo

# %% Types
result_a: ZoneInfo
result_b: ZoneInfo
result_c: ZoneInfo
result_d: ZoneInfo
result_e: ZoneInfo
result_f: ZoneInfo
result_g: ZoneInfo

# %% Data

# %% Result
result_a = ...
result_b = ...
result_c = ...
result_d = ...
result_e = ...
result_f = ...
result_g = ...

# %% About
# - Name: Datetime Timezone ZoneInfo
# - Difficulty: easy
# - Lines: 3
# - Minutes: 3

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% English
# 0. If you're on Windows then install module `tzdata` (`pip install tzdata`)
# 1. Define variable `result_a: ZoneInfo` with 'Poland' timezone
# 2. Define variable `result_b: ZoneInfo` with 'Europe/Warsaw' timezone
# 3. Define variable `result_c: ZoneInfo` with 'Etc/GMT+2' timezone
# 4. Use `zoneinfo`
# 5. Run doctests - all must succeed

# %% Polish
# 0. Jeżeli masz Windows to zainstaluj moduł `tzdata` (`pip install tzdata`)
# 1. Zdefiniuj zmienną `result_a: ZoneInfo` ze strefą czasową 'Poland'
# 2. Zdefiniuj zmienną `result_b: ZoneInfo` ze strefą czasową 'Europe/Warsaw'
# 3. Zdefiniuj zmienną `result_c: ZoneInfo` ze strefą czasową 'Etc/GMT+2'
# 4. Użyj `zoneinfo`
# 5. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `zoneinfo`
# - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# - https://data.iana.org/time-zones/releases/

# %% References
# [1] Wikipedia. List of tz database time zones.
#     Retrieved: 2022-12-01
#     Year: 2022
#     URL: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# [2] IANA. Time Zone Database.
#     Retrieved: 2022-12-01
#     Year: 2022
#     URL: https://data.iana.org/time-zones/releases/

# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from datetime import datetime
>>>
>>> def utcoffset(tz: ZoneInfo):
...     dt = datetime(2000, 1, 1)
...     return tz.utcoffset(dt).total_seconds()


>>> assert result_a is not Ellipsis, \
'Variable `result_a` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_a, ZoneInfo), \
'Variable `result_a` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_a) == 3600, \
'Variable `result_a` is invalid time zone, check IANA Time Zone Database'


>>> assert result_b is not Ellipsis, \
'Variable `result_b` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_b, ZoneInfo), \
'Variable `result_b` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_b) == 3600, \
'Variable `result_b` is invalid time zone, check IANA Time Zone Database'


>>> assert result_c is not Ellipsis, \
'Variable `result_c` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_c, ZoneInfo), \
'Variable `result_c` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_c) == -7200, \
'Variable `result_c` is invalid time zone, check IANA Time Zone Database'
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -f -v myfile.py`

# %% Imports
from zoneinfo import ZoneInfo

# %% Types
result_a: ZoneInfo
result_b: ZoneInfo
result_c: ZoneInfo

# %% Data

# %% Result
result_a = ...
result_b = ...
result_c = ...

# %% About
# - Name: Datetime Timezone ZoneInfo
# - Difficulty: easy
# - Lines: 5
# - Minutes: 5

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% English
# 0. If you're on Windows then install module `tzdata` (`pip install tzdata`)
# 1. Define variable `result_a: ZoneInfo` with timezone for Buenos Aires (Argentina)
# 2. Define variable `result_b: ZoneInfo` with timezone for Tokyo (Japan)
# 3. Define variable `result_c: ZoneInfo` with timezone for Sydney (Australia)
# 4. Define variable `result_d: ZoneInfo` with timezone for Auckland (New Zealand)
# 5. Define variable `result_e: ZoneInfo` with timezone for New York (USA)
# 6. Use `zoneinfo`
# 7. Run doctests - all must succeed

# %% Polish
# 0. Jeżeli masz Windows to zainstaluj moduł `tzdata` (`pip install tzdata`)
# 1. Zdefiniuj zmienną `result_a: ZoneInfo` ze strefą czasową dla Buenos Aires (Argentyna)
# 2. Zdefiniuj zmienną `result_b: ZoneInfo` ze strefą czasową dla Tokio (Japonia)
# 3. Zdefiniuj zmienną `result_c: ZoneInfo` ze strefą czasową dla Sydney (Australia)
# 4. Zdefiniuj zmienną `result_d: ZoneInfo` ze strefą czasową dla Auckland (Nowa Zelandia)
# 5. Zdefiniuj zmienną `result_e: ZoneInfo` ze strefą czasową dla Nowego Jorku (USA)
# 6. Użyj `zoneinfo`
# 7. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `zoneinfo`
# - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# - https://data.iana.org/time-zones/releases/

# %% References
# [1] Wikipedia. List of tz database time zones.
#     Retrieved: 2022-12-01
#     Year: 2022
#     URL: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# [2] IANA. Time Zone Database.
#     Retrieved: 2022-12-01
#     Year: 2022
#     URL: https://data.iana.org/time-zones/releases/

# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from datetime import datetime
>>>
>>> def utcoffset(tz: ZoneInfo):
...     dt = datetime(2000, 1, 1)
...     return tz.utcoffset(dt).total_seconds()


>>> assert result_a is not Ellipsis, \
'Variable `result_a` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_a, ZoneInfo), \
'Variable `result_a` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_a) == -10800.0, \
'Variable `result_a` is invalid time zone, check IANA Time Zone Database'


>>> assert result_b is not Ellipsis, \
'Variable `result_b` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_b, ZoneInfo), \
'Variable `result_b` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_b) == 32400, \
'Variable `result_b` is invalid time zone, check IANA Time Zone Database'


>>> assert result_c is not Ellipsis, \
'Variable `result_c` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_c, ZoneInfo), \
'Variable `result_c` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_c) == 39600, \
'Variable `result_c` is invalid time zone, check IANA Time Zone Database'


>>> assert result_d is not Ellipsis, \
'Variable `result_d` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_d, ZoneInfo), \
'Variable `result_d` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_d) == 46800, \
'Variable `result_d` is invalid time zone, check IANA Time Zone Database'


>>> assert result_e is not Ellipsis, \
'Variable `result_e` has an invalid value; assign result of your program to it.'

>>> assert isinstance(result_e, ZoneInfo), \
'Variable `result_e` has invalid type, must be a `ZoneInfo`'

>>> assert utcoffset(result_e) == -18000.0, \
'Variable `result_e` is invalid time zone, check IANA Time Zone Database'
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -f -v myfile.py`

# %% Imports
from zoneinfo import ZoneInfo

# %% Types
result_a: ZoneInfo
result_b: ZoneInfo
result_c: ZoneInfo
result_d: ZoneInfo
result_e: ZoneInfo

# %% Data

# %% Result
result_a = ...
result_b = ...
result_c = ...
result_d = ...
result_e = ...