Manejo de fechas y horas en Python: datetime explicado

D
DanisCh
(Actualizado: ) • 10 min de lectura
Manejo de fechas y horas en Python: datetime explicado
Python

Trabajar con fechas y horas es una de esas tareas que parece sencilla hasta que te topas con zonas horarias, horario de verano, diferencias entre fechas o conversiones a distintos formatos. Python incluye el módulo datetime en su librería estándar, que cubre la mayoría de casos. Para operaciones más avanzadas con zonas horarias, la librería zoneinfo (incluida desde Python 3.9) y dateutil son los complementos habituales.

Los tipos principales del módulo datetime

from datetime import datetime, date, time, timedelta

# date: solo fecha (año, mes, día) — sin hora
hoy = date.today()
fecha = date(2026, 10, 7)
print(hoy)     # 2026-10-07
print(fecha)   # 2026-10-07

# time: solo hora (hora, minuto, segundo, microsegundo)
hora = time(14, 30, 0)
print(hora)    # 14:30:00

# datetime: fecha + hora juntas
ahora = datetime.now()         # hora local del sistema
ahora_utc = datetime.utcnow()  # hora en UTC (sin info de zona horaria)
dt = datetime(2026, 10, 7, 14, 30, 0)
print(ahora)   # 2026-10-07 14:30:00.123456
print(dt)      # 2026-10-07 14:30:00

# timedelta: duración (diferencia entre dos momentos)
diferencia = timedelta(days=7, hours=3, minutes=30)
print(diferencia)       # 7 days, 3:30:00
print(diferencia.days)  # 7
print(diferencia.total_seconds())  # 621000.0

Crear fechas y horas

from datetime import datetime, date

# Desde componentes individuales
dt = datetime(year=2026, month=10, day=7, hour=14, minute=30, second=0)
# También como posicionales:
dt = datetime(2026, 10, 7, 14, 30, 0)

# Momento actual
ahora = datetime.now()       # hora local
print(ahora.year)            # 2026
print(ahora.month)           # 10
print(ahora.day)             # 7
print(ahora.hour)            # 14
print(ahora.weekday())       # 0=lunes … 6=domingo
print(ahora.isoweekday())    # 1=lunes … 7=domingo

# Inicio y fin del día
from datetime import time
inicio_dia = datetime.combine(date.today(), time.min)   # 00:00:00.000000
fin_dia    = datetime.combine(date.today(), time.max)   # 23:59:59.999999

# Desde timestamp Unix (segundos desde 1970-01-01 UTC)
dt = datetime.fromtimestamp(1728345600)
print(dt)   # 2024-10-07 (aproximadamente)

# El timestamp Unix actual
import time as time_module
ts = time_module.time()   # float con microsegundos
print(ts)   # 1759000000.123456

Formatear fechas: strftime

strftime convierte un objeto datetime en un string con el formato que elijas.

from datetime import datetime

ahora = datetime(2026, 10, 7, 14, 30, 45)

# Formatos más usados
print(ahora.strftime("%Y-%m-%d"))              # 2026-10-07
print(ahora.strftime("%d/%m/%Y"))              # 07/10/2026
print(ahora.strftime("%d de %B de %Y"))        # 07 de octubre de 2026 (en inglés por defecto)
print(ahora.strftime("%H:%M:%S"))              # 14:30:45
print(ahora.strftime("%Y-%m-%dT%H:%M:%S"))    # 2026-10-07T14:30:45  (formato ISO 8601)
print(ahora.isoformat())                       # 2026-10-07T14:30:45  (equivalente)

# Referencia de los códigos más importantes:
# %Y  año con 4 dígitos     (2026)
# %y  año con 2 dígitos     (26)
# %m  mes con cero          (10)
# %d  día con cero          (07)
# %B  nombre del mes        (October)
# %b  nombre abreviado      (Oct)
# %A  nombre del día        (Wednesday)
# %a  nombre abreviado      (Wed)
# %H  hora 24h con cero     (14)
# %I  hora 12h con cero     (02)
# %M  minutos con cero      (30)
# %S  segundos con cero     (45)
# %p  AM / PM               (PM)
# %Z  nombre zona horaria   (UTC, EST…)
# %j  día del año           (280)
# %W  semana del año        (40)

Nombres de mes y día en español

import locale
from datetime import datetime

# Cambiar el locale para obtener nombres en español
# En Linux/Mac:
locale.setlocale(locale.LC_TIME, "es_ES.UTF-8")
# En Windows:
# locale.setlocale(locale.LC_TIME, "Spanish_Spain.1252")

ahora = datetime(2026, 10, 7, 14, 30)
print(ahora.strftime("%A, %d de %B de %Y"))
# miércoles, 07 de octubre de 2026

# Alternativa sin depender del locale del sistema:
MESES = ["", "enero", "febrero", "marzo", "abril", "mayo", "junio",
         "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]
DIAS  = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"]

def fecha_en_espanol(dt: datetime) -> str:
    return f"{DIAS[dt.weekday()]}, {dt.day} de {MESES[dt.month]} de {dt.year}"

print(fecha_en_espanol(ahora))
# miércoles, 7 de octubre de 2026

Parsear fechas: strptime

strptime convierte un string en un objeto datetime. Necesitas especificar el formato exactamente como aparece en el string.

from datetime import datetime

# strptime(string, formato) → datetime
dt = datetime.strptime("2026-10-07", "%Y-%m-%d")
print(dt)   # 2026-10-07 00:00:00

dt = datetime.strptime("07/10/2026 14:30", "%d/%m/%Y %H:%M")
print(dt)   # 2026-10-07 14:30:00

# ISO 8601 (el más habitual en APIs):
dt = datetime.fromisoformat("2026-10-07T14:30:00")
print(dt)   # 2026-10-07 14:30:00

# Con zona horaria en el string (Python 3.11+):
dt = datetime.fromisoformat("2026-10-07T14:30:00+05:00")
print(dt)   # 2026-10-07 14:30:00+05:00

# Error habitual: el formato no coincide con el string
try:
    datetime.strptime("7/10/2026", "%d/%m/%Y")     # OK: 7 sin cero funciona
    datetime.strptime("10-07-2026", "%Y-%m-%d")    # Error: año no es 10
except ValueError as e:
    print(f"Error de formato: {e}")

Parsear formatos desconocidos con dateutil

# pip install python-dateutil
# dateutil.parser.parse detecta el formato automáticamente

from dateutil import parser

fechas = [
    "October 7, 2026",
    "7 Oct 2026",
    "2026-10-07",
    "07/10/2026",
    "Wed, 07 Oct 2026 14:30:00 GMT",
    "2026-10-07T14:30:00Z",
]

for texto in fechas:
    dt = parser.parse(texto)
    print(f"{texto!r:40} → {dt}")

# Útil cuando los datos vienen de fuentes externas con formatos variados
# Advertencia: en formatos ambiguos como "01/02/03",
# dateutil asume MM/DD/YY (comportamiento americano)
# Usa dayfirst=True para forzar DD/MM/YY
dt = parser.parse("01/02/2026", dayfirst=True)   # 1 de febrero
dt = parser.parse("01/02/2026", dayfirst=False)  # 2 de enero (por defecto)

Aritmética con fechas

from datetime import datetime, date, timedelta

hoy = date.today()
ahora = datetime.now()

# Sumar y restar timedelta
manana       = hoy + timedelta(days=1)
ayer         = hoy - timedelta(days=1)
hace_una_hora = ahora - timedelta(hours=1)
en_dos_semanas = hoy + timedelta(weeks=2)
en_90_dias   = hoy + timedelta(days=90)

print(manana)        # 2026-10-08
print(en_dos_semanas) # 2026-10-21

# Diferencia entre dos fechas: resultado es timedelta
fecha1 = date(2026, 1, 1)
fecha2 = date(2026, 10, 7)
diferencia = fecha2 - fecha1
print(diferencia.days)   # 279 días

# Diferencia entre dos datetime
dt1 = datetime(2026, 10, 7,  9, 0, 0)
dt2 = datetime(2026, 10, 7, 14, 30, 0)
delta = dt2 - dt1
print(delta)                    # 5:30:00
print(delta.total_seconds())    # 19800.0
print(delta.seconds // 3600)    # 5 horas
print((delta.seconds % 3600) // 60)  # 30 minutos

# Comparar fechas
print(fecha2 > fecha1)   # True
print(dt1 == dt2)        # False

Sumar meses y años con dateutil

# timedelta no soporta sumar meses directamente (los meses tienen distinta duración)
# dateutil.relativedelta sí lo hace correctamente

from dateutil.relativedelta import relativedelta
from datetime import date

hoy = date(2026, 10, 7)

en_un_mes   = hoy + relativedelta(months=1)    # 2026-11-07
en_un_anio  = hoy + relativedelta(years=1)     # 2027-10-07
hace_3_meses = hoy - relativedelta(months=3)   # 2026-07-07

# Caso especial: fin de mes
fin_enero = date(2026, 1, 31) + relativedelta(months=1)
print(fin_enero)   # 2026-02-28 (ajusta al último día del mes)

# Diferencia en meses entre dos fechas
fecha_inicio = date(2025, 3, 15)
fecha_fin    = date(2026, 10, 7)
delta = relativedelta(fecha_fin, fecha_inicio)
print(f"{delta.years} años, {delta.months} meses, {delta.days} días")
# 1 años, 6 meses, 22 días

Zonas horarias

Un datetime sin zona horaria se llama naive (ingenuo). Un datetime con zona horaria se llama aware (consciente). Mezclar los dos tipos produce errores. En aplicaciones que manejan usuarios en distintos países, siempre trabaja con datetimes aware.

from datetime import datetime, timezone
from zoneinfo import ZoneInfo   # incluido desde Python 3.9

# Datetime naive (sin zona horaria) — evitar en producción
naive = datetime.now()
print(naive.tzinfo)   # None

# Datetime aware en UTC
utc_ahora = datetime.now(timezone.utc)
print(utc_ahora)          # 2026-10-07 19:30:00+00:00
print(utc_ahora.tzinfo)   # UTC

# Datetime aware en una zona horaria específica
bogota  = ZoneInfo("America/Bogota")
madrid  = ZoneInfo("Europe/Madrid")
tokio   = ZoneInfo("Asia/Tokyo")

ahora_bogota = datetime.now(bogota)
ahora_madrid = datetime.now(madrid)

print(ahora_bogota)   # 2026-10-07 14:30:00-05:00
print(ahora_madrid)   # 2026-10-07 21:30:00+02:00

# Convertir entre zonas horarias
ahora_utc   = datetime.now(timezone.utc)
en_bogota   = ahora_utc.astimezone(bogota)
en_tokio    = ahora_utc.astimezone(tokio)

print(en_bogota)   # 2026-10-07 14:30:00-05:00
print(en_tokio)    # 2026-10-08 04:30:00+09:00
# Es el mismo instante, representado en distintas zonas horarias
# Buenas prácticas con zonas horarias

# 1. Guardar siempre en UTC en la base de datos
def guardar_evento(nombre: str, cuando: datetime) -> dict:
    # Convertir a UTC antes de guardar
    cuando_utc = cuando.astimezone(timezone.utc)
    return {
        "nombre":    nombre,
        "timestamp": cuando_utc.isoformat(),   # "2026-10-07T19:30:00+00:00"
    }

# 2. Mostrar al usuario en su zona horaria
def mostrar_al_usuario(timestamp_utc: str, zona_usuario: str) -> str:
    dt_utc = datetime.fromisoformat(timestamp_utc)
    zona   = ZoneInfo(zona_usuario)
    dt_local = dt_utc.astimezone(zona)
    return dt_local.strftime("%d/%m/%Y %H:%M")

hora_mostrar = mostrar_al_usuario("2026-10-07T19:30:00+00:00", "America/Bogota")
print(hora_mostrar)   # 07/10/2026 14:30

# 3. Nunca comparar naive con aware
naive = datetime(2026, 10, 7, 14, 30)
aware = datetime(2026, 10, 7, 14, 30, tzinfo=timezone.utc)

try:
    resultado = naive < aware   # TypeError
except TypeError as e:
    print(f"Error: {e}")
    # can't compare offset-naive and offset-aware datetimes

Casos prácticos habituales

Calcular la edad de una persona

from datetime import date
from dateutil.relativedelta import relativedelta

def calcular_edad(fecha_nacimiento: date) -> int:
    hoy = date.today()
    delta = relativedelta(hoy, fecha_nacimiento)
    return delta.years

def dias_para_cumpleanios(fecha_nacimiento: date) -> int:
    hoy = date.today()
    proximo = fecha_nacimiento.replace(year=hoy.year)
    if proximo < hoy:
        proximo = proximo.replace(year=hoy.year + 1)
    return (proximo - hoy).days

nacimiento = date(1990, 10, 7)
print(calcular_edad(nacimiento))            # 36
print(dias_para_cumpleanios(nacimiento))    # 0 (hoy es su cumpleaños)

Primer y último día del mes

import calendar
from datetime import date

def primer_dia_mes(anio: int, mes: int) -> date:
    return date(anio, mes, 1)

def ultimo_dia_mes(anio: int, mes: int) -> date:
    ultimo = calendar.monthrange(anio, mes)[1]   # (día_semana_inicio, total_días)
    return date(anio, mes, ultimo)

def dias_del_mes_actual():
    hoy = date.today()
    return (
        primer_dia_mes(hoy.year, hoy.month),
        ultimo_dia_mes(hoy.year, hoy.month)
    )

inicio, fin = dias_del_mes_actual()
print(inicio)   # 2026-10-01
print(fin)      # 2026-10-31

Medir el tiempo de ejecución de un bloque de código

import time
from datetime import datetime

# Con time.perf_counter (más preciso para medir rendimiento)
inicio = time.perf_counter()

# ... código que quieres medir ...
import math
resultado = sum(math.sqrt(i) for i in range(1_000_000))

fin = time.perf_counter()
print(f"Tiempo: {fin - inicio:.4f} segundos")   # Tiempo: 0.0821 segundos

# Con contextmanager reutilizable
from contextlib import contextmanager

@contextmanager
def medir_tiempo(etiqueta: str = ""):
    inicio = time.perf_counter()
    yield
    fin = time.perf_counter()
    print(f"{etiqueta}: {(fin - inicio) * 1000:.2f} ms")

with medir_tiempo("consulta SQL"):
    time.sleep(0.05)   # simular consulta

# consulta SQL: 50.12 ms

Agrupar registros por fecha (logs, ventas, etc.)

from datetime import datetime
from collections import defaultdict

registros = [
    {"fecha": "2026-10-07T09:15:00", "importe": 120.0},
    {"fecha": "2026-10-07T14:30:00", "importe":  85.5},
    {"fecha": "2026-10-08T10:00:00", "importe": 200.0},
    {"fecha": "2026-10-08T16:45:00", "importe":  45.0},
    {"fecha": "2026-10-09T11:00:00", "importe": 300.0},
]

ventas_por_dia = defaultdict(float)

for r in registros:
    dt    = datetime.fromisoformat(r["fecha"])
    clave = dt.date()   # solo la parte de fecha
    ventas_por_dia[clave] += r["importe"]

for dia, total in sorted(ventas_por_dia.items()):
    print(f"{dia}: {total:.2f} €")

# 2026-10-07: 205.50 €
# 2026-10-08: 245.00 €
# 2026-10-09: 300.00 €

Resumen

  • El módulo datetime incluye cuatro tipos principales: date (solo fecha), time (solo hora), datetime (fecha + hora) y timedelta (duración). Para la mayoría de operaciones cotidianas no necesitas instalar nada.
  • strftime convierte un datetime en string usando códigos de formato (%Y, %m, %d…). strptime hace el proceso inverso: convierte un string en datetime dado su formato. Para ISO 8601, usa directamente datetime.fromisoformat().
  • La aritmética con fechas usa timedelta para días, horas, minutos y segundos. Para sumar meses o años de forma correcta (respetando meses de distinta duración), usa dateutil.relativedelta.
  • Distingue entre datetimes naive (sin zona horaria) y aware (con zona horaria). En aplicaciones con usuarios en distintos países, guarda siempre en UTC y convierte a la zona del usuario solo para mostrar. El módulo zoneinfo (Python 3.9+) gestiona zonas horarias sin instalar nada extra.
  • dateutil.parser.parse detecta automáticamente el formato de un string de fecha, lo que es útil cuando los datos vienen de fuentes externas con formatos inconsistentes.

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a