¿Por qué Python es lento? (y cuándo no lo es)

“Python es lento”. Lo escuchas constantemente. En benchmarks, en discusiones de Hacker News, en críticas de gente que migró a Go o Rust. Y es medio cierto. Python no es rápido en raw CPU comparado con C, C++, Rust o Go — pero esa comparación, en la práctica, es engañosa.
En la mayoría de proyectos reales, la velocidad pura de la CPU no es el cuello de botella. Lo es el disco, la red, la base de datos, el navegador del usuario. Y cuando sí lo es, Python tiene varios trucos para acelerarlo dramáticamente sin renunciar a la productividad que lo hace agradable.
En esta entrada te explico por qué Python es más lento que lenguajes compilados, cuándo importa de verdad (spoiler: menos de lo que crees) y qué herramientas tienes para acelerarlo cuando hace falta.
Contenido
La razón corta
Python es lento por tres razones principales:
- Es interpretado, no compilado. Cada línea se traduce a operaciones de máquina virtual (bytecode) en runtime. Lenguajes compilados generan binarios optimizados para tu CPU directamente.
- Es de tipado dinámico. En cada operación, Python comprueba qué tipos tiene cada variable.
a + bpuede serint + int,str + str, o lanzar error según los tipos. Esto añade overhead que C no tiene porque el tipo se conoce en compilación. - Tiene el GIL. El Global Interpreter Lock impide que dos threads ejecuten bytecode Python al mismo tiempo. Un programa con threads no aprovecha varios cores en CPU-bound workloads.
💡 La diferencia es entre 5x y 100x según el caso. Operación numérica suelta: 50-100x más lento que C. Llamada a librería bien escrita en C (NumPy, regex…): casi tan rápido como C. La media depende mucho de qué hagas.
Pero la velocidad raw casi nunca es el problema
Mira un proyecto típico:
- Una app web Flask sirve una request → llama a la BBDD → devuelve JSON.
- 95% del tiempo se va en esperar a la BBDD. Que Python sea 30x más lento que Rust en bucles puros no cambia nada: el cuello de botella está en otro sitio.
- Un script ETL → lee CSV de disco, transforma, escribe en BBDD.
- 90% del tiempo es disco y red. Los milisegundos de Python interpretando los strings son ruido frente a los segundos de I/O.
⚡ Regla de oro: antes de obsesionarte con la velocidad de Python, mide. Casi siempre el bottleneck está donde no esperabas. La gente que reescribe scripts en Rust “para más velocidad” descubre que el script tardaba lo mismo porque era I/O-bound.
Cuándo Python sí es lento
Hay tres escenarios donde la lentitud importa:
- CPU-bound puro: procesar millones de números, simulaciones, parsear texto a velocidad bruta sin librerías compiladas. Aquí Python sufre de verdad.
- Latencia ultra baja: sistemas de alta frecuencia, motores de juego, drivers. Aquí Python ni se considera (es C/C++/Rust).
- Throughput masivo en cómputo: entrenamiento de redes neuronales en CPU sin GPU. Por eso TensorFlow y PyTorch hacen el trabajo pesado en C++/CUDA y Python solo orquesta.
Si tu trabajo no cae en ninguno, el bottleneck está en otro sitio.
📥 Llévate el cheatsheet de Python (gratis)
PDF de 6 páginas con lo esencial: tipos, condicionales, bucles, estructuras de datos, funciones y los errores que más vas a cometer. Para tener al lado mientras programas.
Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de Python cada semana.
Las 5 estrategias para acelerar Python
1. Usar librerías compiladas para lo crítico
NumPy, pandas, regex (re), json (con orjson si necesitas más), lxml, scikit-learn — todo lo importante está escrito en C/C++/Rust. Python solo es la interfaz.
# Lento: bucle Python puro
total = 0
for i in range(10_000_000):
total += i * i
# Rápido: NumPy hace el trabajo en C
import numpy as np
total = (np.arange(10_000_000) ** 2).sum()
# 50-100x más rápido. Mismo resultado.
💡 La idea es: escribe Python para la lógica de alto nivel, delega los bucles numéricos a librerías compiladas. Es el patrón estándar en científico/datos.
2. Asyncio para I/O concurrente
Si tu programa pasa el tiempo esperando red/disco, asyncio te permite manejar miles de operaciones simultáneas en un solo thread:
import asyncio
import aiohttp
async def descargar(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as r:
return await r.text()
async def main():
urls = [...] # 1000 URLs
tareas = [descargar(u) for u in urls]
return await asyncio.gather(*tareas)
resultados = asyncio.run(main())
Sin async, descargar 1000 URLs en serie te lleva minutos. Con asyncio, te lleva segundos. Y todo en un solo thread — el GIL no estorba.
💡 Mira la guía completa: asyncio en Python desde cero.
3. Multiprocessing para CPU-bound
El GIL impide threads paralelos en CPU-bound. Pero multiprocesos sí: cada proceso tiene su propio GIL.
from multiprocessing import Pool
def procesar(item):
return item ** 2
if __name__ == "__main__":
with Pool(4) as p: # 4 procesos en paralelo
resultados = p.map(procesar, range(1_000_000))
Acelera linealmente con el número de cores. Coste: cada proceso tiene su propia memoria (overhead más grande) y comunicarse entre procesos requiere serializar.
4. JIT con PyPy
PyPy es un intérprete alternativo de Python con JIT compiler. Para código CPU-bound puro, 5-10x más rápido que CPython sin cambiar una línea.
# Instalar PyPy
brew install pypy3
# Ejecutar con PyPy en vez de python
pypy3 mi_script.py
Limitaciones: no todas las librerías compiladas funcionan (NumPy/scikit-learn van bien, pero otras no). Si tu código es Python puro pesado, PyPy es gratis-ish.
5. Compilar partes críticas con Cython, Numba, mypyc
Cuando un trozo concreto de Python es el bottleneck, lo compilas:
# Numba: decorador que JIT-compila la función
from numba import njit
@njit
def suma_cuadrados(n):
total = 0
for i in range(n):
total += i * i
return total
suma_cuadrados(10_000_000)
# 100x más rápido que Python puro. Misma sintaxis.
- Numba — decorador, ideal para funciones numéricas con NumPy.
- Cython — escribes Python con anotaciones de tipo, compila a C. Más esfuerzo, máximo rendimiento.
- mypyc — compila Python con type hints estrictos a C. Más reciente, prometedor.
⚡ Patrón pro: perfil con
cProfileopy-spy→ identifica las 3 funciones que se llevan el 80% del tiempo → optimiza solo esas. NO optimices a ciegas.
El GIL, en una sección
El Global Interpreter Lock es el lock que asegura que solo un thread Python ejecuta bytecode a la vez. Implicaciones:
- Threads en CPU-bound = inútil. Dos threads que calculan no van más rápido que uno.
- Threads en I/O-bound = útil. Cuando un thread espera red/disco, suelta el GIL y otro avanza.
- Multiprocesos sortean el GIL completamente. Cada uno con su intérprete.
- Asyncio vive todo en un thread, no necesita el GIL.
⚡ Tip-friki: Python 3.13 introduce un build experimental “free-threaded” sin GIL. Aún experimental, pero pinta a que el GIL se va a quitar a medio plazo. Si te toca ver “no-GIL Python” en titulares, viene de aquí.
¿Y si comparo Python con JavaScript / Go / Rust?
Quick reality check sin entrar en flame war:
| Tarea | Python | JS (Node) | Go | Rust |
|---|---|---|---|---|
| Webserver simple | OK | Más rápido | Mucho más rápido | Aún más |
| ML/Data science | El mejor (ecosistema) | Pobre | Pobre | Decente |
| Scripts | El mejor | OK | OK | Costoso |
| Sistemas embebidos | No | No | OK | El mejor |
| Tooling/automatización | Excelente | Bueno | Decente | Decente |
Python no es el más rápido. Es el más rápido para resolver problemas con productividad alta en muchísimas áreas. La velocidad de ejecución importa cuando resuelves el mismo problema 1000 millones de veces. Si lo resuelves una vez al día, lo importante es velocidad de desarrollo.
💡 Más comparativa con JS: Python o JavaScript: cuál elegir.
Errores típicos al pensar en rendimiento Python
- Optimizar antes de medir. “Esto debe ser lento” = casi siempre falso. Mide con
time/cProfiley ataca el 20% que se lleva el 80% del tiempo. - Reescribir en otro lenguaje cuando el problema era I/O. Si esperas la BBDD, da igual qué lenguaje uses. Mejora la query primero.
- Asumir que los threads aceleran código CPU-bound. No, gracias al GIL. Usa
multiprocessingoasynciosegún el tipo de carga. - Usar bucles Python para procesar arrays numéricos. NumPy/pandas hacen lo mismo en C, 50x más rápido.
- Compilar todo con Cython “por si acaso”. Mantenibilidad cae mucho. Solo compila el 5% crítico identificado por profiling.
Resumen
| Causa de lentitud | Cuándo importa | Solución |
|---|---|---|
| Interpretado | CPU-bound puro | NumPy, Numba, Cython, PyPy |
| Tipado dinámico | Bucles numéricos pesados | NumPy, type hints + mypyc |
| GIL | Threads en CPU-bound | Multiprocessing o asyncio |
| Lo que no importa | Casi todo I/O-bound (web, scripts, datos) | Asyncio para concurrencia |
| Antes de optimizar | Siempre | cProfile / py-spy para perfil |
¿Te ha valido esto?
Si te ha resultado útil, llévate el cheatsheet de Python en PDF — 6 páginas con tipos, condicionales, bucles, estructuras de datos, funciones y los errores típicos. Para tener al lado mientras programas. Gratis.
Sin spam. Email + cheatsheet + un correo por semana con tutoriales nuevos.
Tu siguiente paso
Si has llegado hasta aquí, ya sabes que la respuesta a “¿es Python rápido?” es “depende”. Y depende de cosas que tú puedes controlar: usar las librerías adecuadas, no abusar de bucles en hot paths, paralelizar bien según el tipo de carga, y medir antes de optimizar.
Si quieres aprender Python desde la base hasta proyectos reales con rendimiento, profiling y deploy, en El Pythonista lo enseño paso a paso.
Un abrazo,
Oscar
¿Quieres aprender Python en orden, no a saltos?
Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.
37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía
