Fechas, horas y zonas horarias en Python: datetime, pytz y zoneinfo
Python incluye el módulo datetime en la biblioteca estándar y, desde Python 3.9, el módulo zoneinfo para manejar zonas horarias sin dependencias externas. Para proyectos que requieran compatibilidad con Python 3.8 o anterior, pytz sigue siendo la opción más popular.
1. datetime básico: crear y comparar fechas
from datetime import date, time, datetime, timedelta
# Fecha de hoy y hora actual
hoy = date.today()
ahora = datetime.now()
print(hoy) # 2025-06-15
print(ahora) # 2025-06-15 14:30:22.123456
# Crear fechas y horas específicas
reunion = datetime(2025, 9, 1, 10, 30, 0)
print(f"Reunión: {reunion}")
# Comparar
print(reunion > ahora) # True o False según el momento
# Diferencia entre fechas → timedelta
delta = reunion - ahora
print(f"Faltan {delta.days} días y {delta.seconds // 3600} horas")
2. Aritmética con timedelta
from datetime import datetime, timedelta
ahora = datetime.now()
# Sumar / restar
manana = ahora + timedelta(days=1)
hace_semana = ahora - timedelta(weeks=1)
en_dos_horas = ahora + timedelta(hours=2, minutes=30)
print(manana.strftime('%d/%m/%Y %H:%M'))
print(hace_semana.strftime('%d/%m/%Y'))
print(en_dos_horas.strftime('%H:%M'))
# Calcular edad exacta
from datetime import date
def calcular_edad(nacimiento: date) -> int:
hoy = date.today()
return hoy.year - nacimiento.year - (
(hoy.month, hoy.day) < (nacimiento.month, nacimiento.day)
)
print(calcular_edad(date(1990, 3, 25))) # 35 (en 2025)
3. Parsing y formateo: strftime / strptime
from datetime import datetime
# strptime: cadena → datetime
texto = "15/06/2025 14:30"
dt = datetime.strptime(texto, "%d/%m/%Y %H:%M")
print(dt) # 2025-06-15 14:30:00
# strftime: datetime → cadena
print(dt.strftime("%A, %d de %B de %Y")) # Sunday, 15 de June de 2025
print(dt.strftime("%Y-%m-%dT%H:%M:%S")) # ISO 8601
# Formatos comunes en proyectos reales
formatos = [
("20250615", "%Y%m%d"),
("Jun 15, 2025", "%b %d, %Y"),
("2025-06-15T14:30:00", "%Y-%m-%dT%H:%M:%S"),
("15-06-25", "%d-%m-%y"),
]
for texto, fmt in formatos:
dt = datetime.strptime(texto, fmt)
print(f"{texto!r:30} → {dt.date()}")
Códigos strftime más usados
| Código | Significado | Ejemplo |
|---|---|---|
%Y |
Año 4 dígitos | 2025 |
%m |
Mes 01-12 | 06 |
%d |
Día 01-31 | 15 |
%H |
Hora 00-23 | 14 |
%M |
Minuto 00-59 | 30 |
%S |
Segundo 00-59 | 00 |
%A |
Nombre día completo | Sunday |
%B |
Nombre mes completo | June |
%f |
Microsegundos | 000000 |
4. Zonas horarias con zoneinfo (Python ≥ 3.9)
from datetime import datetime
from zoneinfo import ZoneInfo, available_timezones
# Crear datetime "aware" (consciente de zona horaria)
tz_madrid = ZoneInfo("Europe/Madrid")
tz_ny = ZoneInfo("America/New_York")
tz_tokyo = ZoneInfo("Asia/Tokyo")
ahora_madrid = datetime.now(tz_madrid)
print(f"Madrid : {ahora_madrid.strftime('%Y-%m-%d %H:%M %Z%z')}")
# Convertir zona horaria
ahora_ny = ahora_madrid.astimezone(tz_ny)
ahora_tokyo = ahora_madrid.astimezone(tz_tokyo)
print(f"New York: {ahora_ny.strftime('%Y-%m-%d %H:%M %Z')}")
print(f"Tokyo : {ahora_tokyo.strftime('%Y-%m-%d %H:%M %Z')}")
# UTC → local
from datetime import timezone
utc_time = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
local_time = utc_time.astimezone(tz_madrid)
print(f"UTC → Madrid: {local_time.strftime('%H:%M %Z')}") # 14:00 CEST
5. Zonas horarias con pytz (Python 3.6-3.8)
import pytz
from datetime import datetime
tz_madrid = pytz.timezone("Europe/Madrid")
tz_utc = pytz.utc
# IMPORTANTE: usar localize() en lugar de replace() con pytz
ahora_utc = datetime.now(tz_utc)
ahora_madrid = ahora_utc.astimezone(tz_madrid)
print(ahora_madrid.strftime("%Y-%m-%d %H:%M %Z"))
# Crear datetime naive y localizar
naive = datetime(2025, 6, 15, 10, 0, 0)
aware = tz_madrid.localize(naive) # ← CORRECTO con pytz
print(aware.strftime("%Y-%m-%d %H:%M %Z%z"))
# Listar zonas disponibles (subset)
for tz in sorted(pytz.common_timezones)[:5]:
print(tz)
6. Timestamp Unix ↔ datetime
from datetime import datetime, timezone
# datetime → timestamp Unix
dt = datetime(2025, 6, 15, 14, 30, 0, tzinfo=timezone.utc)
ts = dt.timestamp()
print(f"Timestamp: {ts}") # 1750001400.0
# timestamp Unix → datetime
import time
ts_ahora = time.time()
dt_from_ts = datetime.fromtimestamp(ts_ahora, tz=timezone.utc)
print(f"Desde timestamp: {dt_from_ts.isoformat()}")
# Timestamp en milisegundos (común en JS/APIs)
ts_ms = int(ts_ahora * 1000)
dt_from_ms = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
print(f"Desde ms: {dt_from_ms}")
7. ISO 8601 y RFC 3339
from datetime import datetime, timezone, timedelta
# Generar ISO 8601
ahora = datetime.now(timezone.utc)
print(ahora.isoformat()) # 2025-06-15T14:30:22.123456+00:00
print(ahora.strftime("%Y-%m-%dT%H:%M:%SZ")) # Con Z al final
# Parsear ISO 8601 (Python 3.7+)
iso = "2025-06-15T14:30:00+02:00"
dt = datetime.fromisoformat(iso)
print(dt)
print(dt.utcoffset()) # 2:00:00
# Python 3.11+: soporta 'Z' directamente
# dt = datetime.fromisoformat("2025-06-15T14:30:00Z")
# Para versiones anteriores, reemplazar Z
iso_z = "2025-06-15T14:30:00Z"
dt_z = datetime.fromisoformat(iso_z.replace("Z", "+00:00"))
print(dt_z)
8. Comparar fechas con zonas horarias
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# Comparar datetimes aware (ambos deben serlo)
tz_es = ZoneInfo("Europe/Madrid")
tz_us = ZoneInfo("America/New_York")
evento_es = datetime(2025, 6, 15, 20, 0, 0, tzinfo=tz_es) # 20:00 Madrid
evento_us = datetime(2025, 6, 15, 14, 0, 0, tzinfo=tz_us) # 14:00 NY
# Ambos son el mismo instante → True
print(evento_es == evento_us) # True (14:00 NY = 20:00 Madrid CEST)
diferencia = evento_es - evento_us
print(f"Diferencia: {diferencia}") # 0:00:00
# Función: ¿el evento ya pasó?
def ya_paso(evento: datetime) -> bool:
ahora = datetime.now(timezone.utc)
return evento.astimezone(timezone.utc) < ahora
print(ya_paso(evento_es))
9. Fechas en bucles: rangos de fechas
from datetime import date, timedelta
def rango_fechas(inicio: date, fin: date):
"""Genera todas las fechas entre inicio y fin (inclusive)."""
actual = inicio
while actual <= fin:
yield actual
actual += timedelta(days=1)
# Listar todos los lunes de junio 2025
for d in rango_fechas(date(2025, 6, 1), date(2025, 6, 30)):
if d.weekday() == 0: # 0 = lunes
print(d.strftime("%A %d/%m/%Y"))
# Generar fechas de inicio de mes
from datetime import date
import calendar
def primer_dia_mes(año: int, mes: int) -> date:
return date(año, mes, 1)
def ultimo_dia_mes(año: int, mes: int) -> date:
ultimo = calendar.monthrange(año, mes)[1]
return date(año, mes, ultimo)
print(primer_dia_mes(2025, 2)) # 2025-02-01
print(ultimo_dia_mes(2025, 2)) # 2025-02-28
10. Medir tiempo de ejecución
import time
from datetime import datetime
# Opción 1: time.perf_counter() (más preciso para benchmarks)
inicio = time.perf_counter()
# ... tu código aquí ...
time.sleep(0.1)
fin = time.perf_counter()
print(f"Duración: {fin - inicio:.4f} s")
# Opción 2: datetime
t0 = datetime.now()
time.sleep(0.5)
t1 = datetime.now()
delta = t1 - t0
print(f"Duración: {delta.total_seconds():.3f} s")
# Contexto manager para medir bloques
import contextlib
@contextlib.contextmanager
def cronometro(nombre="bloque"):
t0 = time.perf_counter()
yield
t1 = time.perf_counter()
print(f"[{nombre}] {t1 - t0:.4f} s")
with cronometro("procesamiento"):
time.sleep(0.2)
11. dateutil: parsing flexible
# pip install python-dateutil
from dateutil import parser, relativedelta
from datetime import date
# Parsing automático de múltiples formatos
fechas = [
"June 15, 2025",
"15 jun 2025",
"2025/06/15",
"Mon, 15 Jun 2025 14:30:00 +0200",
]
for texto in fechas:
print(parser.parse(texto).date())
# relativedelta: meses/años exactos
hoy = date.today()
en_3_meses = hoy + relativedelta.relativedelta(months=3)
en_1_anio = hoy + relativedelta.relativedelta(years=1)
print(f"En 3 meses: {en_3_meses}")
print(f"En 1 año : {en_1_anio}")
12. Buenas prácticas
- Siempre usa datetimes "aware" en aplicaciones con múltiples zonas horarias; los "naive" causan bugs difíciles de detectar.
- Almacena fechas en UTC en la base de datos; convierte a la zona local solo para mostrar.
- No uses
datetime.utcnow()— no incluye info de zona y puede confundirse con local; usadatetime.now(timezone.utc). - pytz vs zoneinfo: prefiere
zoneinfoen Python 3.9+; usa pytz solo para compatibilidad. - ISO 8601 es el formato estándar para intercambio de datos; úsalo en APIs y archivos.
time.perf_counter()para benchmarks,datetimepara lógica de negocio.
Resumen de módulos
| Módulo | Uso principal | Requiere install |
|---|---|---|
datetime |
Fechas, horas, aritmética | No |
zoneinfo |
Zonas horarias (Py 3.9+) | No |
calendar |
Calendarios, días del mes | No |
time |
Timestamps, benchmarks | No |
pytz |
Zonas horarias (Py < 3.9) | Sí |
dateutil |
Parsing flexible | Sí |
Conversiones relacionadas
Conversiones frecuentes del catálogo: