Handling timezones using the Python standard library is not documented in a clear or easy-to-understand manner, and this results in a lot of blog and stackoverflow posts using 3rd party libs.

There's got to be a better way!

If you read the docs carefully enough, it's possible to achieve a result using only the standard library datetime.

https://docs.python.org/3/library/datetime.html

TL;DR

If you are familiar enough with the datetime lib, the useful methods here are:

  1. astimezone will convert a datetime to another timezone, which changes the hour/day values
  2. replace(tzinfo=x) will override a existing (or None) time zone, while preserving the hour/day values.

And, the silver bullet is:

  • timezone(timedelta(hours=n)) can be used as a tz/tzinfo argument for most of the datetime methods & classes.

I need the current time, in a specific timezone

This is usually utc, but if it's not then people look outside the standard lib. You don't have to!

from datetime import datetime, timezone, timedelta

def current_datetime(utc_offset=0):
     return datetime.now(timezone(timedelta(hours=utc_offset)))
In [1]: str(current_datetime(0))
Out[1]: '2019-02-20 07:56:38.008096+00:00'

In [2]: print(current_datetime(1))
Out[2]: '2019-02-20 08:56:40.968272+01:00'

I need to convert a datetime to a different timezone

from datetime import datetime, timezone, timedelta

def convert_datetime_timezone(dt, utc_offset=0):
    return dt.astimezone(timezone(timedelta(hours=utc_offset)))
In [3]: d = datetimes.current_datetime(2)

In [4]: d
Out[4]: datetime.datetime(2019, 2, 20, 11, 18, 47, 547771, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)))

In [5]: str(d)
Out[5]: '2019-02-20 11:18:47.547771+02:00'

In [6]: str(datetimes.convert_datetime_timezone(d, 3))
Out[6]: '2019-02-20 12:18:47.547771+03:00'

I need to convert a string to a datetime, and force a time zone

When dealing with medium/big data, I sometimes find myself a the situation where I have to read or parse a series of date times that have no time zone, but have to be compared against times from the same or other time zones.

All that's required here is:

  1. You know the exact datetime format that the values will be in
  2. You know what time zone should be assigned

In python this would be called transforming a "time zone naive" object into one that is "time zone aware".

from datetime import datetime, timezone, timedelta

def parse_as_tz(date_str, date_format, utc_offset=0):
    parsed = datetime.strptime(date_str, date_format)
    return parsed.replace(tzinfo=timezone(timedelta(hours=utc_offset)))    
In [7]: str(parse_as_tz(
    ...:     date_str='20180101T10:00:00',
    ...:     date_format='%Y%m%dT%H:%M:%S',
    ...:     utc_offset=1
    ...: ))
Out[7]: '2018-01-01 10:00:00+01:00'

Why is it so difficult to do this?

The crux of the situation is that

  • many timezones are tricky to give concrete, constant definitions to, and
  • using 3rd party libs is any attractive shortcut, as the standard lib documentation is cumbersome and dense.

If it's possible (and easy enough) to do something in the standard library of any language, then that should always be preferable to using a 3rd party option.

Time zones other than UTC aren't supplied by the standard lib. An explanation can be found in the datetime documentation (linked above).

Note that only one concrete tzinfo class, the timezone class, is supplied by the datetime module. [...] Supporting timezones at deeper levels of detail is up to the application.
The rules for time adjustment across the world are more political than rational, change frequently, and there is no standard suitable for every application aside from UTC.

This seems fair enough. The docs then sketch out what it looks like if you want to implement the class structure for a time zone.

class AEST(tzinfo):
    def utcoffset(self, dt):
        return timedelta(hours=10)

    def dst(self, dt):
        return timedelta()
    
    def tzname(self, dt):
        return 'Australia/Melbourne'

This allows you to do neat stuff like access the timezone name and implement daylight savings time (dst), but can be tedious if you have to deal with many timezones where you don't care about the human readable name or the dst.

There are even some intimidating dst implementation examples further down the documentation page, which would be enough to scare off anyone that had managed to read that far.

If you just want to plow through numbers without the boilerplate, then pytz looks like a tempting option. In all seriousness, it's a neat tool.

In summary, useful methods are:

  1. astimezone will convert a datetime to another timezone, which changes the hour/day values
  2. replace(tzinfo=x) will override a existing (or None) time zone, while preserving the hour/day values.

And, the silver bullet is:

  • timezone(timedelta(hours=n)) can be used as a tz/tzinfo argument for most of the datetime methods & classes.

Sleuthing

These are some quick snippets from the datetime doc that lead to my solutions:

  • datetime.astimezone(tz=None)

    If provided, tz must be an instance of a tzinfo subclass, and its utcoffset() and dst() methods must not return None.

  • classmethod datetime.now(tz=None)

    If tz is not None, it must be an instance of a tzinfo subclass, and the current date and time are converted to tz’s time zone.

  • (From the documentation quote used earlier)

    only one concrete tzinfo class, the timezone class, is supplied by the datetime module.

  • class datetime.timezone(offset, name=None)

    The offset argument must be specified as a timedelta object representing the difference between the local time and UTC.