Fechas y horas en Python con datetime y zoneinfo

El módulo datetime lleva en Python desde siempre, y desde la 3.9 trae zoneinfo, que da acceso a la base de datos de husos del sistema sin instalar nada. Con eso está todo.

Las clases, y la distinción que importa

from datetime import date, time, datetime, timedelta, UTC
from zoneinfo import ZoneInfo

d   = date(2026, 10, 7)
t   = time(14, 30)
dt  = datetime(2026, 10, 7, 14, 30)                       # «naive»: SIN zona
mad = dt.replace(tzinfo=ZoneInfo("Europe/Madrid"))        # «aware»: con zona
salidadate          2026-10-07
time          14:30:00
datetime      2026-10-07 14:30:00          <- SIN zona: «naive»
con zona      2026-10-07 14:30:00+02:00
en UTC        2026-10-07 12:30:00+00:00
marca de tiempo 1791376200

Un datetime sin zona no identifica un instante: «el 7 de octubre a las 14:30» ocurre a horas distintas en Madrid y en Tokio. Para registrar cuándo pasó algo, siempre con zona, y lo habitual es guardarlo en UTC.

Aritmética

d + timedelta(days=1)
d - timedelta(weeks=3)
(date(2027, 3, 1) - d).days
d.replace(day=1)
salida+1 día        2026-10-08
-3 semanas    2026-09-16
diferencia    145 días
primer día    2026-10-01
último día    2026-10-31

Restar dos fechas da un timedelta, que se mide en días, segundos y microsegundos. Fíjate en que no hay timedelta(months=1): un mes no dura siempre lo mismo. Para eso está dateutil.relativedelta, o se calcula a mano con calendar.monthrange, que es lo que hace ese «último día».

Formatear y analizar

dt.isoformat()                                          # 2026-10-07T14:30:00
dt.strftime("%d/%m/%Y %H:%M")                           # 07/10/2026 14:30
datetime.strptime("07/10/2026 14:30", "%d/%m/%Y %H:%M")
datetime.fromisoformat("2026-10-07T14:30:00+02:00")
salidaisoformat     2026-10-07T14:30:00
strftime      07/10/2026 14:30
strptime      2026-10-07 14:30:00
fromisoformat 2026-10-07 14:30:00+02:00

Para intercambiar datos, isoformat y fromisoformat: son rápidas, no dependen del idioma y lo que escribe una lo lee la otra. strftime y strptime, solo para hablar con personas o con formatos ajenos.

date.fromisoformat("2026-02-30")ValueError: day 30 must be in range 1..28 for month 2 in year 2026

Husos horarios

for z in ("Europe/Madrid", "America/Mexico_City", "Asia/Tokyo"):
    print(mad.astimezone(ZoneInfo(z)))
salidaEurope/Madrid        2026-10-07 14:30:00+02:00
America/Mexico_City  2026-10-07 06:30:00-06:00
Asia/Tokyo           2026-10-07 21:30:00+09:00

El mismo instante, tres relojes. Y el horario de verano sale solo:

salidaMadrid en enero: 1:00:00   en octubre: 2:00:00
Usa siempre nombres de región —Europe/Madrid— y nunca desplazamientos fijos como +02:00: la región sabe cuándo cambia la hora y el desplazamiento no. En Windows, zoneinfo necesita el paquete tzdata, porque el sistema no trae la base de datos.

El error clásico

salidaTypeError: can't subtract offset-naive and offset-aware datetimes

Mezclar un datetime con zona y otro sin ella no se puede, y es el fallo que más aparece al leer fechas de una base de datos o de una API. La regla que lo evita: con zona en cuanto entra, sin zona nunca. Y para el ahora, datetime.now(UTC), no datetime.utcnow(), que está obsoleto justo porque devolvía un naive.

Quieres… Usa
Hoy date.today()
El instante actual datetime.now(UTC)
Sumar días u horas + timedelta(days=1)
Sumar meses relativedelta, no timedelta
Intercambiar datos isoformat / fromisoformat
Mostrar a una persona strftime
Cambiar de huso astimezone(ZoneInfo(...))

Todo el código se ejecutó con Python 3.14 en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.