Datas locais não identificam sempre um instante único. Mudanças de horário de verão podem repetir ou pular horários. zoneinfo, da biblioteca padrão, usa nomes IANA como America/Sao_Paulo para aplicar regras históricas.
Criar e converter datetimes conscientes
from datetime import UTC, datetime
from zoneinfo import ZoneInfo
agora_utc = datetime.now(UTC)
sao_paulo = agora_utc.astimezone(ZoneInfo("America/Sao_Paulo"))
madri = agora_utc.astimezone(ZoneInfo("Europe/Madrid"))
print(sao_paulo.isoformat())
print(madri.isoformat())
Converta o mesmo instante com astimezone(). Não substitua tzinfo mecanicamente em uma data que já representa outro fuso, pois isso muda a interpretação sem converter o instante.
Armazenar instante e regra de negócio
Para eventos ocorridos, salve um valor consciente em UTC. Para algo como “todo dia às 9h em Madri”, preserve também o fuso IANA, pois o deslocamento UTC muda com as regras locais. Um offset como +02:00 não contém todas essas regras.
Durante uma transição que repete horários, fold diferencia a primeira e a segunda ocorrência. Valide entrada local ambígua em agendamentos críticos. O artigo sobre APScheduler mostra por que declarar o fuso no scheduler.
A documentação oficial de zoneinfo, consultada em 22 de julho de 2026, explica dados IANA, fold e o fallback para tzdata. Teste transições reais dos fusos suportados, não apenas dias comuns.
Instante não é horário civil
Um instante é um ponto na linha do tempo global; horário civil é o valor visto em uma região. Um datetime consciente possui tzinfo capaz de determinar o offset UTC. Um valor ingênuo possui tzinfo=None, portanto o Python não sabe se ele representa UTC, o servidor ou o usuário. Exija esse contexto na entrada.
Use identificadores IANA como America/Sao_Paulo, Europe/Lisbon e Asia/Tokyo. Siglas são ambíguas, e um offset fixo perde regras sazonais e históricas. Valide o identificador:
from zoneinfo import ZoneInfoNotFoundError
def obter_fuso(nome: str) -> ZoneInfo:
permitidos = {"America/Sao_Paulo", "Europe/Lisbon", "Asia/Tokyo"}
if nome not in permitidos:
raise ValueError("fuso não suportado")
try:
return ZoneInfo(nome)
except ZoneInfoNotFoundError as exc:
raise RuntimeError("base de fusos indisponível") from exc
Uma lista permitida atende produtos com mercados definidos. Se qualquer fuso puder ser escolhido, monte a seleção com zoneinfo.available_timezones() e rejeite valores fora do conjunto.
Converter sem reinterpretar
astimezone() preserva o instante e muda sua representação. replace(tzinfo=...) mantém o relógio e atribui outro significado:
reuniao_utc = datetime(2026, 10, 20, 15, 0, tzinfo=UTC)
reuniao_sp = reuniao_utc.astimezone(ZoneInfo("America/Sao_Paulo"))
## Significa 15h em São Paulo; não é conversão.
outro_instante = reuniao_utc.replace(
tzinfo=ZoneInfo("America/Sao_Paulo")
)
Anexar um fuso com replace() é correto quando a entrada confiável declara um horário local e outro campo informa a região. Não serve para mudar a exibição de um instante existente.
Lacunas, repetições e fold
Quando o relógio adianta, certos horários não existem. Quando atrasa, um intervalo ocorre duas vezes. A PEP 495 introduziu fold: zero seleciona o offset anterior e um, o posterior.
fuso = ZoneInfo("America/New_York")
primeiro = datetime(2026, 11, 1, 1, 30, tzinfo=fuso, fold=0)
segundo = datetime(2026, 11, 1, 1, 30, tzinfo=fuso, fold=1)
assert primeiro.timestamp() != segundo.timestamp()
Criar um valor com ZoneInfo não rejeita automaticamente uma entrada inexistente ou ambígua. Em reservas, pagamentos e lembretes médicos, declare a política: peça a ocorrência desejada, desloque segundo regra documentada ou rejeite. Uma validação converte para UTC e volta, comparando relógio e fold.
Guardar dados suficientes
Guarde fatos ocorridos como UTC consciente ou epoch sem ambiguidade, convertendo apenas para exibir. APIs devem emitir ISO 8601 com Z ou offset.
Agendas civis futuras precisam do horário local e do fuso IANA. Converter “dias úteis às 9h em Lisboa” para UTC uma vez e somar 24 horas pode exibir 8h ou 10h após uma transição. Um registro adequado mantém:
hora_local = 09:00
fuso = Europe/Lisbon
recorrencia = dias_uteis
Resolva cada ocorrência com as regras disponíveis no cálculo. Quando houver exigência jurídica ou financeira, salve também o instante UTC resolvido e defina se atualizações alteram ocorrências já geradas.
Interpretar e serializar
datetime.fromisoformat() aceita timestamp com offset, mas offset não equivale a fuso IANA. Se uma API recebe horário e fuso separadamente, valide ambos:
local = datetime.fromisoformat("2026-12-03T09:00:00")
if local.tzinfo is not None:
raise ValueError("era esperado horário local sem offset")
consciente = local.replace(tzinfo=obter_fuso("America/Sao_Paulo"))
instante = consciente.astimezone(UTC)
Use isoformat() na saída e não remova o offset. UTC facilita correlacionar logs; preservar o fuso original permite mostrar data e rótulo esperados.
Dados e testes em produção
zoneinfo procura a base do sistema e depois o pacote oficial tzdata. Windows e contêineres mínimos podem não incluir IANA. Declare tzdata quando necessário e teste uma consulta no health check. Objetos ZoneInfo usam cache; depois de atualizar os dados, reinicie normalmente a aplicação e teste agendas futuras.
Inclua nos testes uma lacuna de avanço, uma repetição de atraso, uma região sem mudança sazonal e zonas que mudam em datas distintas. Compare instantes UTC, não abreviaturas. Governos podem mudar regras, então não transforme o offset atual em premissa permanente.
Antes de publicar, confirme que cada entrada é classificada como instante ou horário local, UTC é consciente, conversões usam astimezone() e eventos civis preservam IANA. A PEP 495, consultada em 28 de julho de 2026, detalha horários ambíguos e fold.
Revisar o fluxo completo
Considere um formulário com 2026-11-01 01:30 e America/New_York. O controller valida formato e fuso permitido. A camada de domínio detecta que o relógio ocorre duas vezes e pede a ocorrência desejada, sem escolher silenciosamente. Depois, cria o valor consciente, converte para UTC e salva tanto o instante quanto o nome IANA. A API responde com timestamp contendo offset; a interface converte o instante salvo para o fuso escolhido pelo leitor.
Esse fluxo evita aplicar o fuso local do servidor quando o cliente omite a região. Rejeitar dados incompletos é mais confiável do que fazer o resultado depender do local onde o contêiner está executando.
Em eventos recorrentes, teste a geração, não apenas a primeira ocorrência. Gere datas atravessando ao menos uma transição e verifique relógio local, instante UTC e política de fold. Se o produto permite mudar o fuso, defina se isso significa “mesmo instante, outra exibição” ou “mesmo horário local em outra região”. São operações diferentes e produzem resultados diferentes.
Datas vindas de banco, JSON e filas também precisam de contrato. Um driver pode devolver datetime ingênuo mesmo para coluna documentada como UTC. Faça uma asserção na camada de persistência e normalize conscientemente. Ao consumir serviço externo, confirme se o sufixo Z, o offset e a precisão são preservados no caminho de ida e volta.
Para mostrar datas, evite usar apenas a sigla do fuso, pois ela pode ser ambígua. Um rótulo como “09:00, horário de São Paulo” comunica melhor. Se a pessoa puder escolher preferências, armazene essa escolha separadamente do fuso de negócio do evento.
Logs estruturados devem usar instantes UTC e incluir o nome IANA relevante em outro campo. Ao investigar divergência, registre a versão da aplicação e do pacote tzdata. Essa evidência diferencia defeito de conversão de atualização intencional das regras.
Uma rotina periódica pode revisar eventos futuros após atualização da base. Ela não deve alterar silenciosamente compromissos confirmados: identifique diferenças, aplique a política do produto e comunique mudanças relevantes. Esse cuidado é especialmente importante em transporte, pagamentos e prazos legais.