Sempre dá para aprender um pouco mais sobre lidar com timezones em Python.

Um jeito que eu estava usando para criar datas com fuso horário era o seguinte:

import pytz
import datetime

timezone = pytz.timezone('America/Sao_Paulo')

aware_date = datetime.datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone)

# datetime.datetime(2020, 1, 1, 0, 0, tzinfo=<DstTzInfo 'America/Sao_Paulo' LMT-1 day, 20\:54\:00 STD>)

Obs: Datas com fuso horário são chamadas de aware.

Esse objeto criado me parecia estranho. O que é esse valor 20:54:00? Por que não há um lindo número redondo? Mas não me preocupei.

Mas algo estranho aconteceu… Quando convertemos essa data para UTC obtemos o seguinte:

aware_date.astimezone(pytz.utc)
# datetime.datetime(2020, 1, 1, 3, 6, tzinfo=<UTC>)

A data/hora em UTC deveria ser 2020-01-01 03:00:00, mas apareceu uma defasagem de 6 minutos. Por que isto?

Isto é uma bizarrice da forma como pytz organiza suas timezones. Aqui tem algumas referências que me foram úteis:

Python pytz timezone function returns a timezone that is off by 9 minutes

pytz localize vs datetime replace

O que entendi que acontece é o seguinte. Para uma dada região nomeada por um fuso horário, pytz armazena todo o histórico de todas as variações de fuso que ocorreram nessa região. Existe um valor específico chamado LMT, ou Local Mean Time, que representa o valor mais antigo desse histórico, antes que os fusos horários do mundo tivessem sido padronizados (no século 19).

Quando você chama puramente pytz.timezone('America/Sao_Paulo), o módulo pytz não sabe qual fuso horário você quer, então ele pode escolher arbitrariamente da lista em função do sistema operacional e talvez versão do pytz. No meu caso, estava retornando esse LMT.

<DstTzInfo 'America/Sao_Paulo' LMT-1 day, 20\:54\:00

De acordo com as perguntas do stackoverflow que listei acima, é errado localizar datas usando o inicializador do datetime passando um timezone do pytz direto no tzinfo. Ou seja:

# ERRADO
aware_date = datetime.datetime(2020, 1, 1, 0, 0, 0, tzinfo=pytz.timezone('America/Sao_Paulo'))

Isto é um problema com o módulo pytz, e não de datetime.

O jeito certo é oferecer ao pytz a informação da data de referência usando o método localize:

naive = datetime.datetime(2020, 1, 1, 0, 0, 0)

# CORRETO
aware = pytz.timezone('America/Sao_Paulo').localize(naive)

# datetime.datetime(2020, 1, 1, 0, 0, tzinfo=<DstTzInfo 'America/Sao_Paulo' -03-1 day, 21\:00\:00 STD>)