Cuando alguien te dice que Python es un lenguaje interpretado, no está mintiendo, pero tampoco está contando la historia completa. La etiqueta es tan imprecisa que, si la usas para razonar sobre rendimiento o portabilidad, te va a llevar a conclusiones equivocadas.
Vamos a ver qué pasa de verdad cuando ejecutas un script de Python.
Lo que ocurre antes de que corra una sola instrucción
Compilación no significa “producir un ejecutable nativo”. Significa transformar código fuente en una representación diferente, normalmente más eficiente de procesar. Python hace exactamente eso, solo que el destino no es código máquina de tu procesador sino bytecode: un conjunto de instrucciones compactas diseñadas para una máquina virtual.
El proceso completo tiene tres etapas:
- Código fuente (
.py) — lo que tú escribes. - Bytecode (
.pyc, guardado en__pycache__/) — compilado por CPython la primera vez que importas el módulo. - Máquina virtual CPython — ejecuta ese bytecode instrucción por instrucción.
La fase de compilación a bytecode ocurre automáticamente y casi siempre es invisible. La carpeta __pycache__ que aparece en tus proyectos es la evidencia física de ese paso. Si modificas el archivo .py, CPython detecta el cambio (vía timestamp y hash) y recompila. Si no lo modificas, reutiliza el .pyc y el arranque es más rápido.
Lo que diferencia a Python de C o Rust no es si compila, sino adónde compila y cuándo. C compila antes de ejecutar, produce código que el procesador entiende directamente, y ese código está ligado a una arquitectura concreta. Python compila en el momento de importar, produce bytecode agnóstico a la arquitectura, y ese bytecode lo ejecuta la VM.
Por qué este diseño importa
Velocidad: la VM de CPython interpreta bytecode, no código máquina nativo. Cada instrucción de bytecode implica varias instrucciones reales del procesador, incluyendo la lógica de despacho de la VM. Por eso Python puro es más lento que C para cómputo intensivo, no porque “no compile” sino porque hay una capa de indirección extra.
Portabilidad: el bytecode es independiente de la arquitectura. El mismo .pyc no viaja entre sistemas (el formato incluye la versión de Python y datos de plataforma), pero el .py sí. Escribes una vez, y cualquier instalación de Python puede recompilarlo a bytecode localmente.
Ciclo de desarrollo: como la compilación es instantánea y automática, el bucle editar → ejecutar es prácticamente inmediato. No hay un paso de build manual que interrumpa el flujo.
La distinción “interpretado vs compilado” es realmente un espectro. Java compila a bytecode y lo ejecuta en la JVM, igual que Python. JavaScript en V8 compila a código máquina justo antes de ejecutar (JIT). Lua compila a bytecode en memoria sin toccar disco. CPython puede generar extensiones en C que corren directamente. ¿Dónde trazar la línea? No hay un punto claro.
Veámoslo en código
import dis
import sys
def add_and_multiply(a, b):
result = a + b
return result * 2
# Muestra el bytecode que CPython generó para esta función
print("=== Bytecode de add_and_multiply ===")
dis.dis(add_and_multiply)
print(f"\n=== Versión de Python: {sys.version} ===")
# El objeto de código lleva consigo las constantes y el bytecode en bruto
code = add_and_multiply.__code__
print(f"Constantes embebidas en el bytecode: {code.co_consts}")
print(f"Variables locales: {code.co_varnames}")
print(f"Bytecode en bytes (primeros 20): {code.co_code[:20]}")
Una ejecución típica produce algo como esto:
=== Bytecode de add_and_multiply ===
5 0 RESUME 0
6 2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 STORE_FAST 2 (result)
7 12 LOAD_FAST 2 (result)
14 LOAD_CONST 1 (2)
16 BINARY_OP 5 (*)
20 RETURN_VALUE
=== Versión de Python: 3.12.3 ...
Constantes embebidas en el bytecode: (None, 2)
Variables locales: ('a', 'b', 'result')
Bytecode en bytes (primeros 20): b'\x97\x00|\x00|\x01\xad\x00...'
Qué nos dice este código
dis es el módulo de la biblioteca estándar que desensambla bytecode a algo legible. Que exista este módulo ya confirma que hay una etapa de compilación real: si Python “solo interpretara” el fuente línea a línea, no habría bytecode que desensamblar.
Fíjate en LOAD_FAST, BINARY_OP, STORE_FAST: son opcodes de la VM de CPython. Cuando ejecutas add_and_multiply(3, 4), la VM lee cada uno de esos opcodes y ejecuta el código C correspondiente dentro del intérprete. Esa es la capa extra respecto a un ejecutable nativo.
code.co_consts muestra que el literal 2 ya está resuelto en tiempo de compilación y embebido en el objeto de código. No se recalcula en cada llamada. Eso es una optimización de la fase de compilación, no algo que haría un “intérprete puro”.
code.co_code son los bytes crudos del bytecode. Su formato es específico a la versión de CPython, razón por la que los archivos .pyc incluyen un número de versión y no son compatibles entre versiones distintas del intérprete.
Errores que debes conocer
Error: asumir que porque Python “es interpretado” los archivos .pyc son prescindibles y se pueden ignorar o borrar sin consecuencias.
# ❌ Limpiar __pycache__ en cada deploy "para estar seguro" # (script de CI que hace rm -rf **/__pycache__ antes de arrancar) # ✅ Dejar que __pycache__ persista entre deploys cuando el # código fuente no cambia; esto acelera el arranque de la aplicación # porque CPython se salta la fase de compilación a bytecode
Borrar __pycache__ no rompe nada, pero obliga a recompilar en el arranque siguiente, lo que añade latencia innecesaria, especialmente en proyectos con muchos módulos.
Error: concluir que Python es lento por ser interpretado y que no hay nada que hacer al respecto.
# ❌ Reescribir todo en C porque "Python no compila" import time data = list(range(1_000_000)) start = time.perf_counter() total = sum(x * 2 for x in data) # bucle a nivel de VM print(time.perf_counter() - start) # ✅ Delegar el cómputo intensivo a extensiones en C que sí corren # en código máquina nativo import numpy as np arr = np.arange(1_000_000) start = time.perf_counter() total = (arr * 2).sum() # ejecuta en C, dentro de NumPy print(time.perf_counter() - start)
La lentitud es de la VM interpretando bytecode, no del lenguaje en sí. NumPy, pandas, o cualquier extensión escrita en C/Fortran/Rust operan fuera de ese cuello de botella. La arquitectura de Python está diseñada exactamente para esto: ser el pegamento entre piezas rápidas.
N° 3