Python 3.12: aceleración incremental y errores que por fin ayudan
Actualizado: 2026-05-03
Python 3.12 salió en octubre de 2023 y, a diferencia de versiones recientes más ruidosas, trajo una lista de mejoras sobrias pero sólidas: sintaxis nueva para genéricos, trazas de error que por fin sirven para depurar, subinterpretes con GIL propio en fase experimental y un rendimiento general en torno al 5% superior a 3.11. Con 3.13 ya publicado y su build sin GIL abriendo conversaciones interesantes, merece la pena detenerse en qué aportó 3.12 realmente al día a día y qué merece pereza o prisa a la hora de migrar.
Puntos clave
- PEP 695 introduce sintaxis de genéricos en línea —
class Cache[T]:en lugar deTypeVaren módulo. - Los tracebacks sugieren el atributo más parecido, resaltan el fragmento exacto e identifican ciclos de importación.
- El proyecto Faster CPython aporta un 5% real en pyperformance; código en C-extensions apenas nota diferencia.
- PEP 684 añade subinterpretes con GIL independiente — útiles en 3.13, no en producción todavía.
- Migrar desde 3.10 o 3.11 es aburrido en el buen sentido: las librerías principales tienen wheels para 3.12.
Lo que cambia sin drama
La estrella de la release es la nueva sintaxis de parámetros de tipo, formalizada en PEP 695. Antes había que importar TypeVar desde typing, declarar la variable en módulo y pasarla a Generic[T] para cada clase genérica. Era ceremonioso y colocaba metadata de tipo lejos del sitio donde tenía sentido leerla. Con 3.12, los corchetes van directamente tras el nombre de la clase o función, en línea, y el intérprete se encarga de crear la variable con el scope correcto. El cambio parece estético, pero reduce fricción real cuando escribes librerías que abusan de genéricos, y además permite declarar alias de tipo con la nueva palabra clave type, evaluados de forma perezosa con soporte de referencias hacia adelante.
El segundo cambio que notas a las dos horas de usar 3.12 son los mensajes de error. Cuando llamas a un método mal escrito, el traceback ya no se limita a decir que el atributo no existe: sugiere el nombre más parecido, remarca el fragmento exacto con caret alignment y, en errores de importación, te indica si falta un paquete o si se trata de un ciclo. Parece cosmético, pero en una base de código grande ahorra minutos por sesión de depuración, especialmente cuando quien lee el error no es quien escribió el código.
Las f-strings recibieron otra mejora silenciosa gracias a PEP 701: ahora son expresiones Python completas. Se pueden anidar comillas del mismo tipo que las exteriores, dividir en varias líneas, incluir comentarios dentro de la interpolación y usar barras invertidas sin tener que recurrir al truco de asignar a una variable antes.
from typing import TypeVar, Generic
# Estilo pre-3.12: ceremonioso, TypeVar en módulo
T_old = TypeVar("T_old")
class Cache(Generic[T_old]):
def get(self, key: str) -> T_old: ...
# 3.12 con PEP 695: el genérico vive junto a la clase
class Cache[T]:
def get(self, key: str) -> T: ...
# Alias de tipo con evaluación perezosa
type Vector = list[float]
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | NoneRendimiento: expectativas realistas
El proyecto Faster CPython sigue dando sus frutos, pero conviene calibrar. Los 5% de mejora agregada frente a 3.11 que anuncia la documentación son reales en benchmarks como pyperformance: DocUtils sube cerca de un 10%, Django un 5% largo, aplicaciones asyncio algo menos. Las comprehensions simples pueden llegar a duplicar su velocidad porque se reescribieron para inlining interno.
Sin embargo, código dominado por llamadas a extensiones en C (numpy, pandas en hot loops, ORMs que pasan tiempo en psycopg) apenas verá diferencia, porque el cuello de botella vive fuera del intérprete.
La lectura pragmática es que 3.12 no es una razón suficiente por sí misma para migrar si tu carga está en I/O o en C, pero tampoco requiere justificación si tu proyecto es mayoritariamente Python puro. En la experiencia habitual, migrar un proyecto Django de tamaño medio desde 3.11 a 3.12 lleva unas dos tardes, la mayoría dedicadas a rebuildar algunas extensiones C que no tenían wheels para 3.12 el primer mes tras el lanzamiento.
Subinterpretes y el futuro post-GIL
PEP 684 introdujo subinterpretes con GIL independiente por cada uno, y 3.12 los expone de forma experimental a través del módulo interpreters. La promesa es interesante: paralelismo real sin salir del proceso, con coste de arranque mucho menor que multiprocessing y sin la sobrecarga de serializar argumentos a través de pipes.
La realidad es que la API está en beta, que buena parte del ecosistema de extensiones C todavía no soporta múltiples intérpretes en el mismo proceso y que los patrones de comunicación por canales están sin estabilizar. Son más útiles como piedra en el camino que como herramienta de producción: preparan el terreno para la build free-threaded que 3.13 introduce como opción de compilación.
Tipado más allá de PEP 695
Hay otras mejoras discretas que alivian código típico:
- PEP 698: el decorador
@overridepermite marcar explícitamente métodos que sobreescriben una clase base, y los type checkers avisan si rompes el contrato por un cambio de nombre en la superclase. - PEP 692: añade
UnpackyTypedDictpara tipar**kwargs, eliminando uno de los agujeros crónicos del tipado estructural. - PEP 669: expone una API de monitorización de bajo coste que herramientas como debuggers y profilers pueden aprovechar sin pagar el precio del antiguo
sys.settrace.
Son mejoras de gente que vive dentro del código a diario: ninguna es espectacular, todas ahorran fricción.
Migración desde 3.10 u 3.11
El camino es aburrido en el buen sentido. La compatibilidad de librerías principales (Django, FastAPI, SQLAlchemy, pandas, numpy) se alcanzó en cuestión de semanas. Las Docker images oficiales python:3.12-slim son drop-in para casi cualquier Dockerfile. El mayor dolor suele venir de extensiones C que dependen de API privadas de CPython: distutils desapareció, funciones en imp fueron eliminadas y algunos paquetes nicho siguen tarde.
La receta recomendada: actualizar pyproject.toml para exigir >=3.12, añadir 3.12 a la matriz de CI en paralelo con la versión actual, dejar que pasen los tests durante una semana y solo entonces retirar la versión antigua. Para proyectos todavía en 3.9, el aviso es diferente: esa rama llega a end of life en octubre de 2025 y acumular dos saltos mayores en poco tiempo multiplica riesgos. Mejor saltar ya a 3.12. Esta cadencia de actualización conecta con los principios de evolución de librerías que analizamos en generics en Go tras tres años — la idea de adopción gradual con pruebas en paralelo aplica a cualquier plataforma.
El panorama con 3.13 ya publicado
Con 3.13 ya publicado, la pregunta honesta es si saltarse 3.12 tiene sentido. La respuesta corta es no: 3.13 aporta el intérprete experimental sin GIL, un JIT igualmente experimental y un REPL reescrito, pero sus mejoras de rendimiento en modo estándar son discretas y la build free-threaded todavía tiene overhead medible en código single-thread.
3.12 es el último peldaño sin sobresaltos antes de que el GIL deje de ser una asunción universal, y es donde deben estar los proyectos que priorizan estabilidad. La transición posterior será más interesante — y más política, porque recompone contratos de concurrencia que llevan décadas en pie —, pero 3.12 te deja en buena forma para afrontarla sin deuda pendiente.
Conclusión
Python 3.12 es la actualización más fácil de justificar en años: mejoras reales sin roturas importantes. PEP 695 hace el tipado genérico legible, los tracebacks mejorados ahorran tiempo de depuración a diario, y el 5% de rendimiento es gratis en proyectos de Python puro. Para equipos en 3.10 o 3.11, la migración es la decisión correcta; para quienes están en 3.9, la urgencia es mayor dada la proximidad del fin de vida.