Inspector de bytecode .pyc de Python
Un .pyc es la forma compilada que CPython escribe junto al fuente, dentro de __pycache__, y tiene exactamente dos partes. Los primeros dieciséis bytes son la cabecera: un número mágico de cuatro bytes que nombra la versión exacta de CPython que escribió el archivo (no la que tengas instalada, sino la que lo compiló) y, a continuación, una palabra de banderas. El bit 0 decide cómo se invalida la caché. A cero, los ocho bytes siguientes guardan la fecha de modificación del fuente y su tamaño, y el sistema de importación tira la caché en cuanto cualquiera de los dos deja de coincidir con el .py del disco. A uno, como permite la PEP 552, esos ocho bytes guardan un hash del fuente, y el bit 1 dice si el intérprete lo vuelve a comprobar en cada importación o si confía a ciegas, que es lo que quiere una compilación reproducible. Detrás de la cabecera hay un único objeto de código serializado con marshal, y cada función, lambda, comprensión y cuerpo de clase del módulo está anidado dentro de sus constantes como otro objeto de código: por eso el árbol baja varios niveles. De cada uno se muestran los nombres que puede alcanzar, las variables locales, de celda y libres, el número de argumentos, el tamaño de pila, los bits de bandera descodificados, cada constante escrita tal como la escribiría repr() y un desensamblado con EXTENDED_ARG plegado en la instrucción a la que pertenece y cada salto resuelto a un desplazamiento absoluto. La numeración de opcodes depende de la versión y cambia casi en cada entrega, así que la tabla se elige a partir del número mágico y se indica en pantalla: van de la 3.7 a la 3.13, y de cualquier otra versión se lee la cabecera y se deja el cuerpo intacto en lugar de descodificarlo con la tabla equivocada. Lo que no hace es devolverte el fuente. Esto no es un descompilador: los nombres, las cadenas de documentación y las constantes sobreviven enteros, pero el flujo que ves es bytecode, no el bucle que escribiste, y los comentarios y el formato los descartó el compilador mucho antes de que existiera este archivo. La tabla de números de línea y, desde la 3.11, la de excepciones se saltan sin descodificar.
Cómo usar
- Suelta un .pyc de una carpeta __pycache__: el nombre suele parecerse a modulo.cpython-311.pyc.
- Mira primero "Escrito por": es la versión de CPython que compiló el archivo, y un intérprete con otro número mágico ignorará la caché y volverá a compilar desde el fuente.
- Comprueba el modo de invalidación. Un archivo por fecha lleva la fecha y el tamaño del fuente; uno por hash lleva un hash del fuente, y la variante sin comprobar ya no se vuelve a contrastar con él.
- Elige un objeto de código del árbol. Arriba está el módulo, y cada función, cuerpo de clase, lambda y (antes de la 3.12) comprensión cuelga de lo que la define.
- Lee el desensamblado del objeto seleccionado: la columna Resuelto sustituye índices por nombres, locales y constantes, y >> marca una instrucción a la que llega algún salto.
Preguntas frecuentes
- He editado el fuente pero Python sigue ejecutando el código antiguo. ¿Por qué?
- Un .pyc invalidado por fecha guarda la modificación del fuente con precisión de un segundo y su tamaño en bytes, y CPython reutiliza la caché mientras ambos sigan coincidiendo. Hay dos situaciones que lo rompen. Si una edición cae dentro del mismo segundo que la fecha registrada y deja el archivo con la misma longitud (un cambio de un carácter en un archivo generado, por ejemplo), nada parece distinto y se usa la caché vieja. Y restaurar archivos con una herramienta que conserva fechas, o cambiar a una rama que las retrocede, puede dejar un .pyc más nuevo que un fuente con el que ya no se corresponde. Borrar la carpeta __pycache__ arregla ambos casos; ejecutar con -B o con PYTHONDONTWRITEBYTECODE evita que se escriban.
- ¿Qué diferencia hay entre un .pyc por hash comprobado y uno sin comprobar?
- La PEP 552 sustituyó la fecha por un hash de ocho bytes del fuente para que la compilación deje de depender de las fechas de los archivos: el mismo fuente produce un .pyc idéntico byte a byte en cualquier máquina, que es lo que necesitan las compilaciones reproducibles. El bit 1 de la palabra de banderas elige el comportamiento: comprobado significa que el intérprete calcula el hash del fuente en cada importación y recompila si difiere; sin comprobar significa que no mira el fuente en absoluto. Lo segundo está pensado para cuando algo ajeno a Python garantiza la frescura, como una imagen de contenedor construida de una vez, y es una trampa en cualquier otro sitio, porque editar el fuente no tiene ningún efecto hasta que se regenera el archivo a mano.
- ¿Puedo recuperar el fuente original a partir de un .pyc?
- Con esta herramienta no, y con ninguna del todo. El compilador conserva todo lo que necesita para ejecutar: cada nombre, cada constante, las cadenas de documentación, los nombres de los argumentos y la primera línea de cada función, así que un .pyc filtra mucho más de lo que la gente supone y nunca debe tomarse por ofuscación. Lo que no conserva es el texto: comentarios, líneas en blanco, formato y la forma exacta de las expresiones desaparecen. Descompiladores como uncompyle6 o decompyle3 reconstruyen un fuente plausible a partir del flujo de instrucciones, pero van años por detrás del lenguaje y dejan de funcionar en torno a la 3.9, porque los compiladores nuevos reestructuran el control de flujo de formas que ya no se traducen uno a uno.
- ¿Por qué el mismo fuente da un bytecode completamente distinto en dos versiones de Python?
- Porque el conjunto de instrucciones es un detalle de implementación que CPython reescribe con libertad. La 3.11 introdujo las cachés en línea, bytes reales dentro del flujo que pertenecen a la instrucción anterior, y trasladó el manejo del marco a opcodes nuevos como RESUME y MAKE_CELL. La 3.12 puso las comprensiones de lista y de conjunto en línea, de modo que dejaron de ser objetos de código aparte. La 3.13 volvió a renumerarlo casi todo. Para eso existe el número mágico: un .pyc solo vale para la versión que lo escribió, y cualquier otro intérprete lo trata como si no estuviera.
- ¿Por qué los desplazamientos saltan de más de dos en dos y qué es EXTENDED_ARG?
- Cada instrucción ocupa dos bytes, uno de opcode y uno de argumento, así que un argumento mayor que 255 se construye con una o varias instrucciones EXTENDED_ARG delante, cada una aportando ocho bits más. Aquí se pliegan en la instrucción siguiente, igual que hace dis, y por eso verás argumentos de miles en una sola fila. Desde la 3.11 el flujo contiene además huecos de caché en línea que pertenecen a la instrucción anterior y guardan el estado de especialización en tiempo de ejecución; se omiten en lugar de listarse, así que los desplazamientos avanzan más de dos. En un .pyc en disco esos huecos siempre valen cero: la especialización ocurre en memoria y los opcodes acelerados nunca se escriben en un archivo.
- ¿Qué son las variables de celda y las libres, y por qué un nombre aparece en dos listas?
- Un cierre necesita que una variable viva más que el marco que la creó, así que el compilador la mete en una celda: la función que la define la lista entre sus variables de celda, y cada función anidada que la lee lista el mismo nombre entre sus variables libres. Desde la 3.11 ambas viven en un único array con un byte de tipo por entrada, y por eso un argumento capturado por una función anidada aparece a la vez como local y como variable de celda: realmente es las dos cosas, y la función empieza con una instrucción MAKE_CELL que traslada el argumento a su celda.
Herramientas relacionadas
Inspector de pickle de Python
Desensambla un .pkl en el navegador: cada opcode, el memo, el valor que reconstruye y si cargarlo ejecutaría código.
Inspector de archivos .class de Java
Analiza un .class compilado en el navegador: versión del archivo y de Java, indicadores de acceso, grupo de constantes, campos, métodos y dependencias.
Inspector de .npy y .npz de NumPy
Lee la cabecera de un .npy o .npz en el navegador: dtype, shape, orden de bytes, campos y una vista de valores, sin NumPy y sin subir nada.
Inspector de binarios ELF
Abre un ejecutable de Linux, un .so o un .o en tu navegador: arquitectura, bibliotecas que necesita, Build ID y si trae PIE, NX y RELRO.
Detector de Tipo de Archivo (Magic Bytes)
Suelta un archivo para identificar su formato real desde sus magic bytes — y capturar archivos cuya extensión o MIME declarado miente.
Inspector de bases de datos SQLite
Abre un archivo .sqlite o .db en tu navegador y lee su estructura: tamaño de página, codificación, modo de diario y las filas reales de cada tabla.