Manipulação de datas com datetime e zoneinfo

1. Introdução ao módulo datetime

O módulo datetime é a biblioteca padrão do Python para manipulação de datas e horas. Suas principais classes são:

  • date: representa uma data (ano, mês, dia)
  • time: representa um horário (hora, minuto, segundo, microssegundo)
  • datetime: combina data e hora
  • timedelta: representa uma diferença entre dois pontos no tempo

Para criar objetos datetime, temos duas abordagens principais:

from datetime import datetime, date, time

# Construtor explícito
data_hora = datetime(2024, 3, 15, 14, 30, 0)
print(data_hora)  # 2024-03-15 14:30:00

# Data e hora atuais
agora = datetime.now()
print(agora)  # 2024-03-15 14:30:45.123456 (exemplo)

# Apenas data
hoje = date.today()
print(hoje)  # 2024-03-15

Os atributos podem ser acessados diretamente:

print(f"Ano: {agora.year}, Mês: {agora.month}, Dia: {agora.day}")
print(f"Hora: {agora.hour}, Minuto: {agora.minute}, Segundo: {agora.second}")

2. Operações com timedelta

O timedelta permite realizar operações aritméticas com datas:

from datetime import datetime, timedelta

hoje = datetime.now()
# Subtração de datas
ontem = hoje - timedelta(days=1)
print(f"Ontem: {ontem}")

# Adição de intervalos
daqui_uma_semana = hoje + timedelta(weeks=1)
print(f"Daqui uma semana: {daqui_uma_semana}")

# Criando timedelta personalizado
intervalo = timedelta(days=5, hours=3, minutes=30)
data_futura = hoje + intervalo
print(f"Daqui 5 dias, 3h e 30min: {data_futura}")

# Diferença entre duas datas
natal = datetime(hoje.year, 12, 25)
dias_restantes = natal - hoje
print(f"Faltam {dias_restantes.days} dias para o Natal")

Cuidado importante: timedelta não lida diretamente com meses ou anos, pois esses períodos não têm duração fixa. Para operações com meses, utilize bibliotecas como dateutil:

from dateutil.relativedelta import relativedelta

hoje = datetime.now()
proximo_mes = hoje + relativedelta(months=1)
print(f"Próximo mês: {proximo_mes}")

3. Formatação e parsing de datas

O método strftime() converte objetos datetime em strings formatadas:

from datetime import datetime

agora = datetime.now()

formatos = {
    "completo": agora.strftime("%Y-%m-%d %H:%M:%S"),
    "brasileiro": agora.strftime("%d/%m/%Y às %H:%M"),
    "extenso": agora.strftime("%A, %d de %B de %Y"),
    "apenas_data": agora.strftime("%d/%m/%Y"),
    "hora_12h": agora.strftime("%I:%M %p"),
}

for nome, formato in formatos.items():
    print(f"{nome}: {formato}")

O método strptime() faz o caminho inverso:

from datetime import datetime

# Conversão de string para datetime
data_str = "15/03/2024 14:30:00"
data_obj = datetime.strptime(data_str, "%d/%m/%Y %H:%M:%S")
print(f"Objeto datetime: {data_obj}")

# Tratamento de diferentes formatos
formatos_possiveis = [
    "%Y-%m-%d",
    "%d/%m/%Y",
    "%d-%m-%Y",
]

def parse_data(texto):
    for formato in formatos_possiveis:
        try:
            return datetime.strptime(texto, formato)
        except ValueError:
            continue
    raise ValueError(f"Formato não reconhecido: {texto}")

print(parse_data("2024-03-15"))  # 2024-03-15
print(parse_data("15/03/2024"))  # 2024-03-15

4. Introdução ao módulo zoneinfo

O módulo zoneinfo, introduzido no Python 3.9, fornece suporte oficial para fusos horários usando o banco de dados IANA (Olson).

from zoneinfo import ZoneInfo
from datetime import datetime

# Criando datetime com fuso horário
sp = ZoneInfo("America/Sao_Paulo")
londres = ZoneInfo("Europe/London")

agora_sp = datetime.now(sp)
agora_londres = datetime.now(londres)

print(f"São Paulo: {agora_sp}")
print(f"Londres: {agora_londres}")

# Diferença entre UTC e horário local
utc = ZoneInfo("UTC")
agora_utc = datetime.now(utc)
print(f"UTC: {agora_utc}")

O banco de dados IANA contém milhares de fusos horários padronizados:

# Listando alguns fusos comuns
fusos = [
    "America/Sao_Paulo",
    "America/New_York",
    "Europe/London",
    "Asia/Tokyo",
    "Australia/Sydney",
]

for fuso in fusos:
    tz = ZoneInfo(fuso)
    agora = datetime.now(tz)
    print(f"{fuso}: {agora}")

5. Conversão entre fusos horários

O método astimezone() converte um datetime para outro fuso horário:

from zoneinfo import ZoneInfo
from datetime import datetime

# Criando datetime aware
sp = ZoneInfo("America/Sao_Paulo")
data_sp = datetime(2024, 3, 15, 14, 30, 0, tzinfo=sp)
print(f"Em SP: {data_sp}")

# Convertendo para Londres
londres = ZoneInfo("Europe/London")
data_londres = data_sp.astimezone(londres)
print(f"Em Londres: {data_londres}")

# Convertendo para Tóquio
toquio = ZoneInfo("Asia/Tokyo")
data_toquio = data_sp.astimezone(toquio)
print(f"Em Tóquio: {data_toquio}")

Diferença entre naive e aware:

from datetime import datetime, timezone

# Naive (sem fuso horário)
naive = datetime(2024, 3, 15, 14, 30)
print(f"Naive: {naive}, tzinfo: {naive.tzinfo}")

# Aware (com fuso horário)
aware = datetime(2024, 3, 15, 14, 30, tzinfo=timezone.utc)
print(f"Aware: {aware}, tzinfo: {aware.tzinfo}")

# Erro comum: tentar converter naive
try:
    naive.astimezone(ZoneInfo("America/Sao_Paulo"))
except ValueError as e:
    print(f"Erro: {e}")

Horário de verão:

from zoneinfo import ZoneInfo
from datetime import datetime

sp = ZoneInfo("America/Sao_Paulo")

# Durante horário de verão (outubro)
verao = datetime(2023, 10, 15, 14, 30, tzinfo=sp)
print(f"Verão: {verao}, UTC offset: {verao.utcoffset()}")

# Fora do horário de verão (junho)
inverno = datetime(2023, 6, 15, 14, 30, tzinfo=sp)
print(f"Inverno: {inverno}, UTC offset: {inverno.utcoffset()}")

6. Trabalhando com UTC e timestamps

Timestamps Unix representam segundos desde 01/01/1970:

from datetime import datetime, timezone

# Obtendo timestamp
agora = datetime.now(timezone.utc)
timestamp = agora.timestamp()
print(f"Timestamp Unix: {timestamp}")

# Convertendo timestamp para datetime
data_do_timestamp = datetime.fromtimestamp(timestamp, tz=timezone.utc)
print(f"De volta ao datetime: {data_do_timestamp}")

# Boa prática: armazenar em UTC
def salvar_data():
    utc_now = datetime.now(timezone.utc)
    return utc_now.isoformat()

def exibir_local(iso_string, fuso_local):
    data_utc = datetime.fromisoformat(iso_string)
    data_local = data_utc.astimezone(fuso_local)
    return data_local.strftime("%d/%m/%Y %H:%M")

# Uso
from zoneinfo import ZoneInfo
data_para_banco = salvar_data()
print(f"Armazenado (UTC): {data_para_banco}")
print(f"Exibido (SP): {exibir_local(data_para_banco, ZoneInfo('America/Sao_Paulo'))}")

7. Casos especiais e boas práticas

Comparações entre objetos datetime:

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# Datas são imutáveis e comparáveis
data1 = datetime(2024, 3, 15, tzinfo=timezone.utc)
data2 = datetime(2024, 3, 16, tzinfo=timezone.utc)

print(f"data1 < data2: {data1 < data2}")
print(f"data1 == data2: {data1 == data2}")

# Comparação entre fusos diferentes funciona corretamente
sp = ZoneInfo("America/Sao_Paulo")
data_sp = datetime(2024, 3, 15, 20, 0, tzinfo=sp)
data_utc = datetime(2024, 3, 15, 23, 0, tzinfo=timezone.utc)
print(f"data_sp == data_utc: {data_sp == data_utc}")  # True, mesmo horário

Dicas para projetos reais:

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# 1. Sempre armazene em UTC
def get_utc_now():
    return datetime.now(timezone.utc)

# 2. Converta para fuso local apenas na exibição
def format_for_user(dt_utc, user_timezone="America/Sao_Paulo"):
    tz = ZoneInfo(user_timezone)
    local_dt = dt_utc.astimezone(tz)
    return local_dt.strftime("%d/%m/%Y %H:%M:%S")

# 3. Para APIs, use ISO 8601
def to_iso_string(dt):
    return dt.isoformat()

# 4. Valide datas recebidas
def parse_iso_date(iso_string):
    try:
        return datetime.fromisoformat(iso_string)
    except ValueError:
        raise ValueError(f"Data inválida: {iso_string}")

# Exemplo de uso
utc_now = get_utc_now()
print(f"UTC: {utc_now}")
print(f"Para usuário: {format_for_user(utc_now)}")
print(f"ISO: {to_iso_string(utc_now)}")

Performance: Para operações intensivas, considere usar datetime.timestamp() e trabalhar com números inteiros, que são mais rápidos que objetos datetime.


Referências