Python

Trabalhando com Datas e Horas Já leu

8 min de leitura

Trabalhando com Datas e Horas
Datas e horas estão em todo lugar no desenvolvimento de software — registros de log, validade de tokens, agendamentos, relatórios por período, cálculo de prazos. Python oferece um conjunto robusto de ferramentas para man

Datas e horas estão em todo lugar no desenvolvimento de software — registros de log, validade de tokens, agendamentos, relatórios por período, cálculo de prazos. Python oferece um conjunto robusto de ferramentas para manipular tempo, mas o tema tem armadilhas importantes: fusos horários, horário de verão e formatos de string são fontes clássicas de bugs sutis em sistemas reais.


O Módulo datetime

O módulo principal para trabalhar com datas e horas:

from datetime import date, time, datetime, timedelta

# date — apenas data
hoje = date.today()
print(hoje)          # 2024-03-15
print(hoje.year)     # 2024
print(hoje.month)    # 3
print(hoje.day)      # 15

# time — apenas hora
agora_hora = time(14, 30, 45)
print(agora_hora)        # 14:30:45
print(agora_hora.hour)   # 14

# datetime — data e hora combinadas
agora = datetime.now()
print(agora)             # 2024-03-15 14:30:45.123456

# Criando datetime específico
aniversario = datetime(1989, 7, 4, 8, 0, 0)
print(aniversario)   # 1989-07-04 08:00:00

# Data de hoje como datetime
hoje_dt = datetime.today()
print(hoje_dt.date())    # 2024-03-15
print(hoje_dt.time())    # 14:30:45.123456

timedelta: Aritmética com Datas

timedelta representa uma duração — a diferença entre dois instantes:

from datetime import datetime, timedelta

agora = datetime.now()

# Somando e subtraindo
amanha        = agora + timedelta(days=1)
semana_passada = agora - timedelta(weeks=1)
daqui_2_horas  = agora + timedelta(hours=2)
daqui_90_min   = agora + timedelta(minutes=90)

print(amanha)
print(semana_passada)

# Diferença entre datas
nascimento   = datetime(1990, 5, 20)
hoje         = datetime.today()
diferenca    = hoje - nascimento

print(f"Dias vividos: {diferenca.days}")
print(f"Anos aproximados: {diferenca.days // 365}")

# timedelta tem: days, seconds, microseconds
delta = timedelta(days=2, hours=3, minutes=30)
print(delta.total_seconds())  # 95400.0

Formatação: strftime e strptime

Converter entre objetos datetime e strings:

from datetime import datetime

agora = datetime.now()

# strftime — datetime → string (format time)
print(agora.strftime("%d/%m/%Y"))           # 15/03/2024
print(agora.strftime("%d/%m/%Y %H:%M"))     # 15/03/2024 14:30
print(agora.strftime("%Y-%m-%dT%H:%M:%S"))  # 2024-03-15T14:30:45
print(agora.strftime("%A, %d de %B de %Y")) # Friday, 15 de March de 2024

# strptime — string → datetime (parse time)
texto   = "25/12/2024 08:00"
natal   = datetime.strptime(texto, "%d/%m/%Y %H:%M")
print(natal)
print(type(natal))  # <class 'datetime.datetime'>

# ISO 8601 — formato padrão para APIs
iso = agora.isoformat()
print(iso)   # 2024-03-15T14:30:45.123456

# Parsing de ISO 8601 (Python 3.7+)
dt = datetime.fromisoformat("2024-03-15T14:30:45")
print(dt)

Principais códigos de formato:

%Y  ano com 4 dígitos       2024
%m  mês com zero à esquerda 03
%d  dia com zero à esquerda 15
%H  hora 24h                14
%M  minutos                 30
%S  segundos                45
%A  dia da semana           Friday
%B  nome do mês             March
%p  AM ou PM                PM
%f  microssegundos          123456

Fusos Horários com zoneinfo

Datas sem fuso horário são chamadas de naive — funcionam bem para uso local, mas são problemáticas em sistemas distribuídos. Datas com fuso horário são chamadas de aware.

from datetime import datetime
from zoneinfo import ZoneInfo   # Python 3.9+

# datetime naive — sem fuso horário
naive = datetime.now()
print(naive.tzinfo)   # None

# datetime aware — com fuso horário
fuso_sp  = ZoneInfo("America/Sao_Paulo")
fuso_utc = ZoneInfo("UTC")
fuso_ny  = ZoneInfo("America/New_York")

agora_sp  = datetime.now(tz=fuso_sp)
agora_utc = datetime.now(tz=fuso_utc)

print(agora_sp)           # 2024-03-15 14:30:45-03:00
print(agora_utc)          # 2024-03-15 17:30:45+00:00

# Convertendo entre fusos
agora_ny = agora_sp.astimezone(fuso_ny)
print(agora_ny)           # 2024-03-15 14:30:45-04:00

# Regra prática: armazene sempre em UTC, exiba no fuso do usuário
def agora_utc_iso():
    return datetime.now(tz=ZoneInfo("UTC")).isoformat()

print(agora_utc_iso())    # 2024-03-15T17:30:45.123456+00:00

O Módulo calendar

import calendar

# Calendário de um mês
print(calendar.month(2024, 12))

# Verificações
print(calendar.isleap(2024))   # True — 2024 é bissexto
print(calendar.isleap(2023))   # False

# Dias no mês
print(calendar.monthrange(2024, 2))  # (3, 29) — começa na 4ª feira, 29 dias

# Dia da semana (0=segunda, 6=domingo)
print(calendar.weekday(2024, 7, 4))  # 3 = quinta-feira

Medição de Tempo com time e perf_counter

import time

# time.time() — timestamp Unix (segundos desde 1970-01-01)
inicio = time.time()
time.sleep(0.1)   # pausa de 100ms
fim    = time.time()
print(f"Decorrido: {fim - inicio:.3f}s")

# perf_counter — maior precisão para benchmarks
inicio = time.perf_counter()
resultado = sum(i ** 2 for i in range(1_000_000))
fim    = time.perf_counter()
print(f"Processamento: {fim - inicio:.4f}s")

# time.sleep() — pausa a execução
print("Aguardando...")
time.sleep(2)
print("Retomando.")

# Timestamp atual
print(time.time())         # 1710512245.123
print(int(time.time()))    # 1710512245

dateutil: Parsing Flexível

Para cenários onde o formato da data é incerto ou variado, a biblioteca python-dateutil é indispensável:

pip install python-dateutil
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta

# Parsing inteligente de qualquer formato
datas = [
    "15/03/2024",
    "March 15, 2024",
    "2024-03-15",
    "15 de março de 2024",
    "03-15-2024",
    "15.03.2024",
]

for texto in datas:
    try:
        dt = parse(texto, dayfirst=True)
        print(f"{texto:30} → {dt.strftime('%d/%m/%Y')}")
    except Exception as e:
        print(f"{texto:30} → Erro: {e}")

# relativedelta — diferenças em anos, meses e dias
nascimento = datetime(1990, 5, 20)
hoje       = datetime.today()
idade      = relativedelta(hoje, nascimento)

print(f"Idade: {idade.years} anos, {idade.months} meses, {idade.days} dias")

# Somando meses e anos corretamente
hoje = datetime(2024, 1, 31)
proximo_mes = hoje + relativedelta(months=1)
print(proximo_mes)   # 2024-02-29 — ajusta para o último dia do mês

Exemplo Completo: Sistema de Agendamentos

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from dataclasses import dataclass, field
from typing import List
import uuid


FUSO = ZoneInfo("America/Sao_Paulo")


@dataclass
class Compromisso:
    titulo:    str
    inicio:    datetime
    duracao:   timedelta
    descricao: str = ""
    id:        str = field(default_factory=lambda: str(uuid.uuid4())[:8])

    @property
    def fim(self) -> datetime:
        return self.inicio + self.duracao

    @property
    def duracao_minutos(self) -> int:
        return int(self.duracao.total_seconds() / 60)

    def conflita_com(self, outro: "Compromisso") -> bool:
        return self.inicio < outro.fim and self.fim > outro.inicio

    def __str__(self):
        return (f"[{self.id}] {self.titulo}\n"
                f"  Início: {self.inicio.strftime('%d/%m/%Y %H:%M')}\n"
                f"  Fim:    {self.fim.strftime('%d/%m/%Y %H:%M')}\n"
                f"  Duração: {self.duracao_minutos} minutos")


class Agenda:
    def __init__(self, nome: str):
        self.nome          = nome
        self._compromissos: List[Compromisso] = []

    def agendar(self, compromisso: Compromisso) -> bool:
        for existente in self._compromissos:
            if compromisso.conflita_com(existente):
                print(f"Conflito com: '{existente.titulo}'")
                return False
        self._compromissos.append(compromisso)
        print(f"Agendado: '{compromisso.titulo}'")
        return True

    def compromissos_do_dia(self, data: datetime) -> List[Compromisso]:
        return sorted(
            [c for c in self._compromissos
             if c.inicio.date() == data.date()],
            key=lambda c: c.inicio
        )

    def proximos(self, n: int = 5) -> List[Compromisso]:
        agora = datetime.now(tz=FUSO)
        futuros = [c for c in self._compromissos if c.inicio >= agora]
        return sorted(futuros, key=lambda c: c.inicio)[:n]

    def exibir_dia(self, data: datetime):
        compromissos = self.compromissos_do_dia(data)
        print(f"\n=== {self.nome} — {data.strftime('%d/%m/%Y')} ===")
        if not compromissos:
            print("  Nenhum compromisso.")
        for c in compromissos:
            print(c)


# Uso
agenda = Agenda("Ricardo Matos")
hoje   = datetime.now(tz=FUSO).replace(hour=0, minute=0, second=0, microsecond=0)

compromissos = [
    Compromisso(
        titulo="Reunião de planejamento",
        inicio=hoje.replace(hour=9, minute=0),
        duracao=timedelta(hours=1),
    ),
    Compromisso(
        titulo="Almoço com cliente",
        inicio=hoje.replace(hour=12, minute=0),
        duracao=timedelta(hours=1, minutes=30),
    ),
    Compromisso(
        titulo="Code review",
        inicio=hoje.replace(hour=14, minute=0),
        duracao=timedelta(minutes=45),
    ),
    Compromisso(
        titulo="Conflito — tenta marcar às 9h30",
        inicio=hoje.replace(hour=9, minute=30),
        duracao=timedelta(hours=1),
    ),
]

for c in compromissos:
    agenda.agendar(c)

agenda.exibir_dia(hoje)

print(f"\n=== Próximos {len(agenda.proximos())} compromissos ===")
for c in agenda.proximos():
    faltam = c.inicio - datetime.now(tz=FUSO)
    print(f"  {c.titulo} — em {faltam.seconds // 3600}h {(faltam.seconds % 3600) // 60}min")

Resumo

  • date, time e datetime são os tipos fundamentais do módulo datetime
  • timedelta representa durações e permite aritmética entre datas
  • strftime converte datetime → string; strptime converte string → datetime
  • Use sempre ISO 8601 (isoformat()) para armazenar e transmitir datas em APIs
  • Datas naive não têm fuso; datas aware têm — prefira aware em sistemas distribuídos
  • zoneinfo (Python 3.9+) é a forma moderna de trabalhar com fusos horários
  • python-dateutil simplifica parsing de formatos variados e cálculos com meses e anos
  • Armazene sempre em UTC; converta para o fuso do usuário apenas na exibição

Referências e Leituras Complementares

  • Módulo datetime — documentação oficial — https://docs.python.org/3/library/datetime.html
  • zoneinfo — fusos horários (PEP 615) — https://docs.python.org/3/library/zoneinfo.html
  • python-dateutil — https://dateutil.readthedocs.io/en/stable/
  • Banco de dados de fusos horários IANA — https://www.iana.org/time-zones
  • PEP 615 — zoneinfo — https://peps.python.org/pep-0615/
  • BEAZLEY, David; JONES, Brian K. Python Cookbook. 3. ed. O'Reilly Media, 2013. Cap. 3 — receitas para datas, horas e cálculos temporais.
  • AKINSHIN, Andrey. Pro .NET Benchmarking. Apress, 2019. — referência complementar sobre medição precisa de tempo em software.
Comentários

Mais em Python

Autenticação e Segurança em APIs Python
Autenticação e Segurança em APIs Python

Uma API sem autenticação é uma porta aberta. Segurança não é um recurso opcio...

Algoritmos de Ordenação e Busca
Algoritmos de Ordenação e Busca

Ordenar e buscar dados s&atilde;o duas das opera&ccedil;&otilde;es mais funda...

Concorrência: threads, processos e async/await
Concorrência: threads, processos e async/await

Todo programa que vimos até aqui executa uma instrução por vez — modelo seque...