Cuando se importa un módulo Python, el intérprete puede guardar su bytecode en un archivo .pyc dentro de __pycache__. El módulo py_compile en Python permite generar este caché anticipadamente, validar archivos durante builds y controlar cómo el intérprete decide si el bytecode todavía corresponde al código fuente.
Esta guía explica py_compile.compile(), PyCompileError, niveles de optimización e invalidación por timestamp o hash. Complementa nuestros artículos sobre bytecode con dis, compileall, tracebacks, tokenize y compilación interactiva.
Qué es un archivo pyc
Un archivo .pyc contiene bytecode serializado y un encabezado con información para validar el caché. Puede evitar recompilar código sin cambios en determinados arranques, pero no convierte Python en un binario nativo independiente.
El bytecode sigue siendo específico de la implementación y versión. Un archivo creado para una etiqueta de caché de CPython no debe considerarse compatible con otra.
Compilar un archivo fuente
La función principal recibe la ruta del archivo.
import py_compile
ruta = py_compile.compile("mi_modulo.py")
print(ruta)Por defecto, el resultado sigue las convenciones PEP 3147 y PEP 488, normalmente dentro de __pycache__ con la etiqueta del intérprete.
Validar sintaxis durante el build
La compilación anticipada permite detectar SyntaxError antes del despliegue.
from pathlib import Path
import py_compile
for archivo in Path("src").rglob("*.py"):
py_compile.compile(
str(archivo),
doraise=True,
)Para árboles enteros, compileall es más cómodo. py_compile resulta apropiado cuando la aplicación ya dispone de una lista exacta.
doraise y PyCompileError
Por defecto, un error se escribe en stderr y la función devuelve None. Con doraise=True, se lanza PyCompileError.
try:
py_compile.compile(
"roto.py",
doraise=True,
)
except py_compile.PyCompileError as error:
print("La compilación falló:", error)Las aplicaciones y pipelines deberían preferir la excepción para controlar el estado y producir reportes estructurados.
Elegir el destino con cfile
cfile selecciona explícitamente el archivo de salida.
py_compile.compile(
"modulo.py",
cfile="build/modulo.pyc",
doraise=True,
)Crea antes el directorio. Las rutas personalizadas pueden servir para empaquetado, mientras las convenciones estándar facilitan la importación y convivencia entre versiones.
Protección frente a symlinks
Si el destino calculado es un enlace simbólico o archivo no regular, la función lanza FileExistsError.
La documentación oficial de py_compile explica que la escritura sigue la semántica de importlib, mediante archivo temporal y renombrado para reducir resultados parciales en concurrencia. La comprobación evita reemplazar objetos especiales silenciosamente.
Nombre fuente en tracebacks
dfile define el nombre almacenado para tracebacks y mensajes.
py_compile.compile(
"src/paquete/modulo.py",
dfile="/app/paquete/modulo.py",
doraise=True,
)Es útil cuando la ruta de build difiere de la ruta de despliegue. Usa una ubicación significativa sin exponer directorios privados.
Nivel de optimización
optimize se pasa a compile(). El valor predeterminado -1 selecciona el nivel del intérprete actual.
py_compile.compile(
"modulo.py",
optimize=1,
doraise=True,
)El nivel 1 elimina asserts y cambia __debug__. El nivel 2 también elimina muchas docstrings. No uses optimización para ocultar código ni supongas una mejora relevante sin medir.
Cachés separados por optimización
Con el destino estándar, la etiqueta incluye la variante de optimización. Varios niveles pueden coexistir dentro de __pycache__.
Si eliges cfile manualmente, evita sobrescribir variantes necesarias. Registra versión, implementación y optimización en el build.
Modos de invalidación
PycInvalidationMode determina cómo Python comprueba que un .pyc está actualizado.
TIMESTAMP: compara timestamp y tamaño del fuente;CHECKED_HASH: almacena un hash y lo verifica al importar;UNCHECKED_HASH: almacena un hash, pero confía en un sistema externo.
El modo queda registrado en el encabezado.
Invalidación por timestamp
from py_compile import PycInvalidationMode
py_compile.compile(
"modulo.py",
doraise=True,
invalidation_mode=PycInvalidationMode.TIMESTAMP,
)Es rápido y suele ser el valor predeterminado cuando SOURCE_DATE_EPOCH no está definido. Sistemas de archivos con baja resolución temporal pueden producir casos límite.
Hash verificado
py_compile.compile(
"modulo.py",
doraise=True,
invalidation_mode=PycInvalidationMode.CHECKED_HASH,
)El intérprete vuelve a calcular el hash del fuente al importar. Mejora el determinismo y evita depender solo de timestamps, con coste adicional de lectura y hashing.
Hash no verificado
py_compile.compile(
"modulo.py",
doraise=True,
invalidation_mode=PycInvalidationMode.UNCHECKED_HASH,
)Python asume que el caché es correcto. Úsalo solo cuando un gestor de paquetes o build system actualiza rigurosamente los archivos.
SOURCE_DATE_EPOCH
Cuando SOURCE_DATE_EPOCH está definida, el modo predeterminado pasa a CHECKED_HASH. Esto ayuda a builds reproducibles que no deben depender de timestamps reales.
Desde Python 3.7.2, la variable determina el default, pero no sustituye un argumento explícito.
El parámetro quiet
quiet controla mensajes cuando doraise=False.
- 0 o 1: diagnóstico normal;
- 2: sin mensajes y
doraisedeja de tener efecto.
La automatización debería preferir doraise=True y manejar la excepción.
Interfaz de terminal
El módulo puede compilar archivos nombrados explícitamente.
python -m py_compile archivo1.py archivo2.pyNo busca recursivamente en directorios. El código de salida es distinto de cero si algún archivo no se compila.
Leer nombres desde stdin
Cuando - es el único argumento, los nombres se leen de la entrada estándar.
find src -name '*.py' -print | python -m py_compile -En Unix, los nombres pueden contener newlines. Un pipeline estricto puede preferir recorrer rutas con Path.
Modo quiet en la CLI
python -m py_compile -q archivo1.py archivo2.pyLa opción reduce mensajes, pero el pipeline todavía debe comprobar el status.
Permisos e instalaciones compartidas
La compilación anticipada es útil cuando los usuarios pueden leer el paquete, pero no escribir en __pycache__.
El instalador con privilegios adecuados crea los cachés. Después, los permisos deberían permitir lectura sin otorgar escritura innecesaria.
Escrituras concurrentes
La estrategia temporal y el renombrado reduce archivos parciales cuando varios procesos compilan el mismo destino. No hace seguro dirigir fuentes distintas a un mismo cfile.
Usa directorios separados por intérprete y evita jobs concurrentes sobre un artefacto personalizado compartido.
Los pyc no protegen la lógica
El bytecode puede inspeccionarse y desmontarse. Distribuir solamente .pyc no proporciona protección fuerte de propiedad intelectual, firma ni cifrado.
El bytecode no confiable sigue siendo peligroso cuando se importa, igual que el fuente.
Eliminar cachés antiguos
La mayoría de proyectos no debería versionar __pycache__. Al cambiar de intérprete o crear un build limpio, elimina y regenera.
from pathlib import Path
for carpeta in Path(".").rglob("__pycache__"):
for archivo in carpeta.iterdir():
archivo.unlink()
carpeta.rmdir()Usa la limpieza con cuidado en entornos compartidos.
Helper controlado
from pathlib import Path
import py_compile
def compilar(archivo: Path) -> Path:
salida = py_compile.compile(
str(archivo),
doraise=True,
optimize=0,
invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH,
)
return Path(salida)El llamador puede registrar tamaño, digest y versión de Python.
Pruebas
Crea fuentes temporales válidas e inválidas y verifica destinos, excepciones y política de invalidación.
from tempfile import TemporaryDirectory
with TemporaryDirectory() as carpeta:
fuente = Path(carpeta) / "ok.py"
fuente.write_text("valor = 42\n", encoding="utf-8")
pyc = compilar(fuente)
assert pyc.exists()Prueba también un destino symlink en plataformas compatibles.
Errores frecuentes
- Suponer que pyc es portable entre versiones.
- Ignorar
Nonecondoraise=False. - Usar una salida personalizada para varias fuentes.
- Elegir hash no verificado sin un build confiable.
- Distribuir pyc como protección del fuente.
- Confundir compilación con pruebas funcionales.
- No registrar el nivel de optimización.
- Versionar cachés locales sin necesidad.
Buenas prácticas
- Usa
doraise=Trueen automatización. - Prefiere rutas estándar de
__pycache__. - Selecciona la invalidación explícitamente para builds reproducibles.
- Separa artefactos por intérprete y optimización.
- Compila antes del despliegue para detectar sintaxis.
- Ejecuta tests además de compilar.
- No importes bytecode no confiable.
- Limpia cachés al cambiar de entorno.
Conclusión
El módulo py_compile en Python compila fuentes en cachés .pyc, valida sintaxis y prepara instalaciones donde el runtime no puede escribir en el paquete. Controla destino, nombre de traceback, optimización e invalidación.
La elección entre timestamps y hashes depende del build. Con excepciones explícitas, rutas convencionales y artefactos aislados por versión, py_compile hace predecible la generación de bytecode sin confundir un caché con ejecutable portable, protección del código o prueba funcional.







