PEP 810 – Importaciones diferidas explícitas
(pep-previews--4622.org.readthedocs.build)- En Python, la convención habitual es declarar todas las importaciones a nivel de módulo
- Sin embargo, al ejecutar un programa también se cargan de inmediato módulos de dependencias innecesarias, lo que provoca problemas de velocidad de arranque y uso de memoria
- Hasta ahora se usaban mucho las importaciones diferidas manuales, por ejemplo dentro de funciones, pero esto tiene la desventaja de dificultar el mantenimiento y la gestión de dependencias
- Este PEP 810 introduce una sintaxis de importaciones diferidas explícitas con la nueva palabra clave
lazy, de forma local, explícita, controlada y granular - Con esta función, los módulos se cargan solo cuando realmente se necesitan, mejorando la latencia de inicio y el desperdicio de memoria al mismo tiempo que se mantiene la transparencia en la estructura del código
Situación actual y problemas de las importaciones en Python
- En Python, la práctica más extendida es escribir las sentencias
importal inicio del módulo - Este enfoque reduce duplicación, permite ver de un vistazo la estructura de dependencias de importación e importar una sola vez para minimizar la sobrecarga en tiempo de ejecución
- Sin embargo, cuando se carga el primer módulo (
main) al ejecutar el programa, es fácil que se produzcan importaciones en cadena que terminan cargando de inmediato muchos módulos de dependencia que en realidad no se usan - En particular, en las herramientas CLI, incluso invocar solo la ayuda puede precargar decenas de módulos, generando sobrecarga innecesaria para cada subcomando
Alternativas existentes y sus problemas
- Con frecuencia se retrasa manualmente el momento de importar, por ejemplo moviendo las importaciones dentro de funciones
- Pero este enfoque tiene desventajas importantes, como pérdida de consistencia y mantenibilidad, además de dificultar la comprensión de todas las dependencias
- Según el análisis de la biblioteca estándar, en código sensible al rendimiento ya se usa diferimiento de importaciones con fines de optimización en alrededor del 17% de todas las importaciones, colocándolas dentro de funciones o métodos
- Existen herramientas relacionadas con el diferimiento de importaciones, como
importlib.util.LazyLoadery el paquete de terceroslazy_loader, pero no cubren todos los casos o no existe un estándar único
PEP 810: introducción de importaciones diferidas explícitas
-
Se introduce la nueva palabra clave suave
lazy(solo tiene significado en contextos específicos y también puede usarse como nombre de variable) -
lazysolo puede usarse antes de una sentenciaimport, y no puede usarse en ámbitos de función/clase/with/try ni constar import -
Se aplica de forma clara por cada sentencia de importación, retrasando la carga del módulo hasta el momento de uso
lazy import nombre_del_módulo lazy from nombre_del_módulo import nombre
Forma de implementación y reglas sintácticas de las importaciones diferidas explícitas
-
Casos que producen error de sintaxis:
- No se permite dentro de funciones, dentro de clases, en
try/with, ni constar import(*)
- No se permite dentro de funciones, dentro de clases, en
-
Ejemplo de uso:
import sys lazy import json print('json' in sys.modules) # False (todavía no se carga) result = json.dumps({"hello": "world"}) # Se carga en el primer uso print('json' in sys.modules) # True (carga diferida del módulo completada) -
También se puede indicar el objetivo lazy en el atributo
__lazy_modules__a nivel de módulo, como lista de cadenas__lazy_modules__ = ["json"] import json # se procesa como lazy
Control del comportamiento mediante bandera global y filtros
-
Es posible controlar si se aplica
lazypor módulo o de forma global usando una bandera global o una función de filtrado -
También se puede usar una función de filtrado para aplicar excepciones de importación eager solo a módulos específicos
def my_filter(importer, name, fromlist): if name in {'problematic_module'}: return False # eager import return True # lazy import sys.set_lazy_imports_filter(my_filter)
Comportamiento en runtime y manejo de errores
-
Cuando se usa
lazy import, la importación real ocurre no en la sentencia de importación, sino en el primer acceso al nombre -
Si la importación falla, se muestra claramente tanto el lugar donde se definió como donde ocurrió mediante encadenamiento de excepciones (
traceback chaining)lazy from json import dumsp # error tipográfico result = dumsp({"key": "value"}) # ImportError en el momento del acceso real
Beneficios de memoria y rendimiento
- Los módulos diferidos aparecen solo en el conjunto
sys.lazy_modulesy no se registran ensys.modulesantes de su uso real - Después de usarse, se reemplazan por objetos de módulo normales y pueden usarse sin penalización adicional de rendimiento
- En cargas de trabajo reales, se observa una reducción de 50 a 70% en la latencia de arranque y un ahorro de 30 a 40% de memoria
Resumen del funcionamiento
- En el primer acceso a un objeto lazy ocurre la reificación (
reification), es decir, la importación real y el reemplazo - Si código externo accede al
__dict__del módulo, todos los objetos lazy se cargan de forma forzada (reificación) - Al extraer el diccionario con
globals(), el proxy lazy se mantiene y requiere acceso directo
Anotaciones de tipo y optimización de TYPE_CHECKING
- Con
lazy from módulo import nombre, las importaciones usadas solo para tipos garantizan costo de runtime CERO - Esto permite reemplazar condicionales existentes con
from typing import TYPE_CHECKING, haciendo el código más conciso y claro
Diferencias frente a PEP 690 y características de implementación
- PEP 810 adopta una estructura explícita, por importación individual y opt-in, basada en objetos proxy simples
- En cambio, PEP 690 proponía una estructura de importaciones diferidas global e implícita
Puntos de atención e interacción entre módulos
star import(*) no tiene soporte lazy (siempre es eager)- Los
import hookyloaderpersonalizados siguen funcionando normalmente en el momento de la reificación - Incluso en entornos multihilo, se garantiza que la importación se realice una sola vez de forma thread-safe y con enlace seguro
- Si se usa el mismo módulo en modo lazy y eager al mismo tiempo, el eager siempre tiene prioridad
Guía de aplicación del código y migración
- Al aplicar esto en código existente, se recomienda convertir a lazy solo las importaciones necesarias mediante profiling y hacerlo de forma gradual
- Si se usa
__lazy_modules__, hay compatibilidad incluso con versiones anteriores a Python 3.15
Otros puntos importantes de preguntas y respuestas
- Los efectos secundarios en tiempo de importación (por ejemplo, patrones de registro) se retrasan hasta el primer acceso. Si el side effect es indispensable, se recomienda un patrón con función de inicialización explícita
- El problema de circular import no se resuelve por completo con lazy import (solo puede mitigarse si el acceso también se retrasa)
- El rendimiento en rutas críticas elimina por completo la verificación lazy después del primer uso mediante optimización automática (
bytecode adaptive specialization) - El módulo real solo se registra en
sys.modulesdespués de la reificación (primer uso) - A diferencia de
importlib.util.LazyLoader, no requiere configuración adicional, mantiene el rendimiento y ofrece claridad mediante sintaxis estándar
Conclusión
- PEP 810 agrega la palabra clave
lazya las sentenciasimportde Python, permitiendo optimizar de forma simple y predecible los problemas de rendimiento causados por la carga innecesaria de módulos en áreas como CLI con subcomandos, aplicaciones grandes y anotaciones de tipo - La nueva palabra clave permite especificar con precisión el momento y el objetivo de adopción, por lo que resulta adecuada para introducción gradual y ajuste de rendimiento en servicios reales
- Como una evolución práctica del sistema de importación de Python, satisface al mismo tiempo las tres necesidades de visibilidad, mantenibilidad y rendimiento
1 comentarios
Comentarios en Hacker News
Mi herramienta CLI de llm.datasette.io soporta plugins, pero hubo muchas quejas de que incluso comandos como "llm --help" tenían un arranque demasiado lento. Revisando, resultó que plugins populares importaban por defecto paquetes pesados como pytorch, bloqueando todo el inicio. Por eso en la documentación para autores de plugins indico que importen dependencias solo dentro de las funciones y solo cuando hagan falta (enlace a la documentación relacionada), pero creo que sería mucho mejor que Python soportara esto a nivel del lenguaje.
Hoy mismo se puede implementar esto en herramientas (enlace con explicación), pero este enfoque se aplica globalmente a todo el proceso, así que si importas
numpyde forma diferida, también se diferirán todos los imports de sus submódulos. Al final, si no se necesita todonumpy, puede que ni siquiera llegue a importarse, pero el fenómeno de importar módulos parcialmente en el momento en que se necesiten puede quedar repartido de manera impredecible durante la ejecución. En pruebas adicionales, si hacesimport foo.bar.baz,fooyfoo.barsiguen cargándose de inmediato y solofoo.bar.bazqueda diferido. Quizá esa sea una de las razones por las que el PEP usa la palabra "mostly". Si mejorara más mi implementación, tal vez podría resolver eso.Recomiendo parsear primero la línea de comandos para manejar opciones como "--help" sin importar nada. Ejecuta los imports solo cuando realmente sean necesarios; o, dicho de otra forma, diseña el programa para importar solo cuando ya se hayan procesado las opciones sencillas y todavía quede trabajo por hacer.
La propuesta de lazy import ya existía desde antes, y la más reciente fue rechazada en 2022 (enlace a la discusión relacionada). Según recuerdo, lazy import ya está en Cinder, la variante de CPython de Meta, y esta vez también están impulsando el PEP personas que trabajaron en Cinder. La discusión se centró en cosas como: "¿opt-in u opt-out?", "¿hasta dónde se aplica?", "¿debería ir como build flag de CPython?". Al final, dicen que el Steering Council lo rechazó por la complejidad de dividir el comportamiento de los imports en dos. Ojalá esta propuesta sí pase; realmente quiero usar esta función.
Me gusta especialmente que sea opt-in, con aplicación granular por niveles e incluso un interruptor global para desactivarlo. Es una especificación muy bien armada dentro de muchas restricciones.
Yo también espero que esta propuesta pase, pero no soy optimista. Esto va a romper muchísimo código y va a desatar problemas inesperados. La sentencia
importtiene efectos secundarios por naturaleza, y si cambia el momento en que se aplican, vamos a sufrir durante mucho tiempo con bugs cuya causa no será obvia. No es alarmismo, es una preocupación real y con fundamento. Hay una razón por la que lazy import solo estaba en Meta: hace falta tener recursos al nivel de Meta para lidiar con algo así. Mucha gente solo ve "pandas, numpy o mi weird module enredado son demasiado lentos, ojalá fueran más rápidos", pero creo que muy poca gente entiende siquiera cómo funciona realmente el sistema de imports de Python. Incluso hay muchas opiniones a favor sin saber cómo se implementaría lazy import. Si miras PEP 690, hay varias desventajas. Por ejemplo, se rompe código que usa decoradores para registrar funciones en un registry central. Un caso típico es la librería Dash, que conecta interfaces basadas en JavaScript con callbacks de Python mediante decoradores al momento del import; si el import se vuelve lazy, ese frontend simplemente dejaría de funcionar. Hasta servicios con muchísimos usuarios podrían romperse al instante. Y dicen: “como es opt-in, si no te sirve, apagas lazy import”. ¿Pero qué pasa si los imports son transitivos? ¿Qué pasa si necesitas arrancar un proceso importante solo después de que el frontend quede completamente inicializado? ¿Quién sabe qué efectos puede tener eso en un ecosistema donde se mezclan librerías y código de muchas personas? A diferencia de los type hints, esto sí cambia de verdad el comportamiento en runtime. La sentenciaimportestá en prácticamente todo código Python real, así que si se introduce lazy import, cambia de forma fundamental la manera de ejecutar. Y además hay más casos raros de los que habla el PEP. Es un problema mucho más difícil de lo que parece.Sería buenísimo poder hacer imports con versión explícita como
import torch==2.6.0+cu124,import numpy>=1.2.6, y además poder instalar/importar varias versiones de un mismo paquete al mismo tiempo dentro del mismo entorno de Python. Ya quisiera que se acabara este infierno de conda/virtualenv/docker/bazel.No me desagrada, pero tampoco me entusiasma demasiado. Si esto sigue así, siento que vamos a terminar poniendo
lazydelante de casi todos losimport, salvo unos pocos casos que de verdad necesiten eager import. Eso ensucia el código, y como tampoco parece que vaya a convertirse en el comportamiento por defecto, esa verbosidad se quedará para siempre. Yo habría preferido un sistema donde el módulo declarara opt-in para lazy loading, sin cambiar la sintaxis deimport. Así solo las librerías grandes tendrían que preocuparse por la pereza. Claro, eso implicaría que el intérprete tenga que inspeccionar el sistema de archivos al momento de importar, entre otras desventajas.Si todo el mundo pudiera usar lazy import ampliamente sin mayores problemas, entonces lazy debería haber sido el valor por defecto y <i>eager</i> debería ser la keyword opcional. Este tipo de cambio de paradigma no sería el primero en Python: varias construcciones que en v2 creaban listas eager pasaron a ser generators en v3 sin causar mayores problemas.
Si hubiera una bandera de línea de comandos para volver lazy todos los imports de módulos de Python, yo la usaría sin dudar. En la práctica, fuera de scripts o código realmente simple, generar side effects al cargar un módulo es un patrón que de verdad debería evitarse.
No creo que tenga sentido que el módulo decida si usar lazy loading. Solo quien llama sabe si necesita lazy load, así que lo razonable es que la opción esté del lado del código que hace el import. Cualquier módulo podría cargarse de forma lazy, y aunque tenga side effects, quien lo importa podría querer diferir también eso.
Ojalá se pudiera especificar una opción de lazy loading por regex en
pyproject.toml.En el pasado la gente expresó preocupaciones parecidas cada vez que salieron funciones nuevas como type hints, walrus, asyncio, dataclasses, etc., pero en la práctica no fue como que muchísima gente empezara a usarlas todas de golpe ni que todos los patrones existentes cambiaran. Muchos usuarios siguen usando básicamente un Python modernizado al nivel de 2.4, y aun así son suficientemente productivos. Lleva 20 años funcionando bien, así que no creo que haya un problema tan grande.
Si te interesa, presento lazyimp, que implementa lazy import de forma muy cómoda usando un context manager. Normalmente basta con envolver la sentencia
importen un bloquewith, así que se lleva bien con las herramientas existentes, y si hace falta depurar, se puede volver fácilmente a eager import. Usa una cext para cambiarf_builtinsdel frame y ser más potente que un hook de importlib. No es perfecto, pero también tiene una versión thread-safe y una versión con handler global. Al principio era cauteloso, pero ahora ya migré casi toda mi base de código a esto y no he tenido ningún problema real (salvo no haber cuidado el registro por módulo), además de que la mejora de velocidad se siente muchísimo, así que estoy muy satisfecho.Es realmente molesto que los linters de Python fuercen a poner los imports al principio del archivo. Cada vez que uno usa la forma obvia de implementar lazy import, aparecen errores de lint. Este problema va mucho más allá de un simple tema de rendimiento. Por ejemplo, si necesitas una librería específica de una plataforma y quieres importarla solo en esa plataforma, forzar el import al principio puede hacer que el import falle por completo.
En ese caso, creo que no queda otra que arreglar el linter.
La mayoría de los linters se pueden ignorar con un comentario como
#noqa E402.Con esto reemplazas el meta path finder por un wrapper, de modo que el loader se sustituye por
LazyLoader. Cuando se ejecuta el import, el nombre del módulo en realidad queda enlazado como<class 'importlib.util._LazyModule'>, y el módulo real se carga al acceder a sus atributos. Código de prueba:Eso sí, no tengo claro qué significa exactamente "mostly" en el PEP.
Me parece que se están subestimando los riesgos de thread safety en los lazy imports. No se puede predecir en absoluto cuándo se ejecutará un import, en qué hilo, ni qué locks tendrá tomados, aparte del importer lock. Antes, aunque se ejecutara código riesgoso al importar un módulo, casi siempre ocurría solo durante la inicialización monohilo, así que no era tan problemático. Si todo pasa a lazy, los errores podrían aparecer de formas realmente impredecibles, tipo Heisenbug. Los imports a nivel de función también pueden tener ese problema, pero al menos tienen la previsibilidad de ejecutarse al principio de código explícito.
Se siente como una buena función. Es fácil de explicar, tiene casos de uso reales y su alcance (global, o con una keyword simple) parece razonable. Me gusta.
Entre los PEP recientes, siento que este es de los más limpios desde la perspectiva del usuario. Tengo curiosidad por ver el resultado final después del tradicional proceso de syntax bikeshedding.
Me parece un PEP cuidadosamente preparado: validación con casos de uso reales y edge cases, compromisos razonables, un enfoque no exagerado y varias rondas de refinamiento. Sobre todo porque tocar un sistema tan central como este en un lenguaje grande con comunidades de todo tipo en el mundo puede ser muy peligroso. Considerando esa dificultad, me parece especialmente impresionante.
Espero que hayan aprendido bien por qué se rechazó PEP-690. En nuestra base de código también intentamos implementar algo así por cuenta propia, pero nunca logramos que funcionara bien a un nivel realmente utilizable.
El peligro de lazy import es que puede producir errores inesperados en runtime en servicios de larga duración. Parece una ventaja por el arranque rápido, pero a cambio te llevas la posibilidad de que la ejecución se detenga a la mitad por un fallo de import. Además, pueden aparecer edge cases donde ya no sea posible garantizar qué cosas se importarán al arrancar el programa.
Aun así, este es un problema real que sí o sí hay que resolver. No es solo por la velocidad de arranque; cuando Python arranca con dependencias grandes, puede volverse ridículamente lento. Los proyectos grandes tampoco pueden simplemente empaquetar todas las librerías pesadas que no todos los usuarios van a usar, así que los desarrolladores ya están recurriendo a soluciones todavía más raras, lo cual añade problemas absurdos. Ya sería un gran avance con solo dejar de duplicar, esconder o disimular imports a nivel de función. Y además, esto se está proponiendo únicamente como una feature opcional del lenguaje.
Con pruebas automatizadas se puede mitigar bastante el riesgo, y vale la pena a cambio de un arranque rápido. El startup time nunca es solo un problema "cosmético". Yo lo sufrí en un monolito de Django: por culpa de apenas unas cuantas librerías pesadas, había que esperar entre 10 y 15 segundos en cada management command, test o recarga de contenedor. Diferir esos imports con lazy import hizo una diferencia enorme.
Nosotros preferimos imports explícitos al principio del archivo, porque así los problemas de dependencias salen a la luz en cuanto arranca el programa. Si usas lazy import, aparece la incomodidad de descubrir el problema solo cuando se ejecuta cierta ruta de código, quizá horas o días después.
pipen tareas cortas mejoraría de inmediato.La mayor parte del tiempo en realidad se va importando y descargando módulos vendor que ni siquiera se usan (por ejemplo, casi 100 módulos solo relacionados con Requests). Al investigarlo, vi que en total se importan innecesariamente más de 500 módulos.
Tampoco sé por qué los generadores de código están produciendo cada vez más código que mete imports locales dentro de funciones en vez de ponerlos arriba. No me gustaría recomendar ese patrón, porque vuelve más difícil entender las dependencias del módulo y aumenta el riesgo de que después aparezcan dependencias cíclicas.
Aún no leo todo el PEP, pero pienso que estaría bien si hubiera alguna forma de hacer validación de dependencias mediante una bandera de línea de comandos o una herramienta externa, algo parecido a las herramientas para type hints.
Me da curiosidad a quién se refiere exactamente "nosotros".
¿No sería esto algo que debería cubrirse con tests?