Una consola interactiva debe decidir si el texto introducido ya forma una instrucción Python completa. Después de if condicion:, por ejemplo, debería mostrar el prompt de continuación en lugar de intentar ejecutar inmediatamente. El módulo codeop en Python proporciona las utilidades de compilación usadas por los REPL para distinguir código completo de un prefijo válido pero incompleto y para conservar instrucciones __future__ entre comandos.
Esta guía explica compile_command(), Compile y CommandCompiler, incluido el manejo de errores y el diseño de un loop interactivo controlado. Complementa nuestros artículos sobre bytecode con dis, ámbitos con symtable, tracebacks, inspect y Python IDLE.
El problema que resuelve un REPL
Un ciclo read-eval-print lee texto, lo compila, lo ejecuta y muestra un resultado. La dificultad es que una línea puede estar completa, ser inválida o simplemente necesitar más líneas.
if usuario_activo:
conceder_acceso()Después de la primera línea, el parser espera un bloque indentado. Una consola debe responder con ..., acumular la entrada y volver a intentar la compilación.
compile_command()
codeop.compile_command() intenta compilar una cadena como entrada interactiva.
import codeop
resultado = codeop.compile_command("x = 10")
print(resultado)Si el código está completo y es válido, devuelve un objeto de código. Si es un prefijo válido que todavía necesita contenido, devuelve None. Una sintaxis inválida provoca una excepción.
Distinguir incompleto de inválido
entradas = [
"x = 10",
"if x > 0:",
"if :",
]
for texto in entradas:
try:
codigo = codeop.compile_command(texto)
except SyntaxError as error:
print("Inválido:", error)
else:
if codigo is None:
print("Incompleto")
else:
print("Completo")Esta distinción de tres estados es la razón principal para usar codeop en vez de llamar directamente al built-in compile().
El argumento filename
El nombre de archivo aparece en mensajes de sintaxis y tracebacks posteriores.
codigo = codeop.compile_command(
"resultado = 10 / 0",
filename="<consola-admin>",
)Elige un identificador útil como <consola>, <regla-42> o la ruta real de un script. No incluyas secretos ni datos personales en el filename.
El argumento symbol
symbol selecciona el modo de compilación:
single: una instrucción interactiva, valor predeterminado;exec: una secuencia de instrucciones;eval: una expresión.
expresion = codeop.compile_command(
"10 * 2",
symbol="eval",
)
print(eval(expresion, {}))Cualquier otro valor genera ValueError. La aplicación debe seleccionar el modo según su interfaz y no permitir que un cliente no confiable lo controle libremente.
single y la presentación del resultado
El modo single está pensado para comportamiento interactivo. Los resultados de expresiones pueden enviarse al display hook del intérprete, como en la consola estándar.
codigo = codeop.compile_command("2 + 3", symbol="single")
exec(codigo)Una interfaz web o gráfica normalmente capturará la salida y aplicará su propia representación.
Construir un acumulador de líneas
Un loop sencillo mantiene un buffer hasta que el código queda completo.
import codeop
buffer = []
while True:
prompt = "... " if buffer else ">>> "
linea = input(prompt)
buffer.append(linea)
fuente = "\n".join(buffer)
try:
codigo = codeop.compile_command(fuente)
except (SyntaxError, OverflowError, ValueError) as error:
print(f"Error: {error}")
buffer.clear()
continue
if codigo is None:
continue
exec(codigo, globals(), globals())
buffer.clear()El ejemplo demuestra la mecánica, pero ejecutar entrada arbitraria mediante exec() es peligroso. Una consola remota necesita aislamiento y autorización estricta.
Líneas en blanco
En el intérprete tradicional, una línea vacía finaliza un bloque indentado. Conserva los saltos de línea y deja que compile_command() tome la decisión.
if buffer and linea == "":
fuente = "\n".join(buffer) + "\n"Prueba funciones, clases, try, with, decorators, strings multilínea, paréntesis abiertos y bloques async.
Errores de compilación
La función puede lanzar SyntaxError por sintaxis inválida y OverflowError o ValueError por determinados literales y parámetros.
try:
codigo = codeop.compile_command(fuente)
except SyntaxError as error:
mostrar_error_sintaxis(error)
except (OverflowError, ValueError) as error:
mostrar_error_compilacion(error)La documentación oficial de codeop también advierte sobre casos poco comunes en los que el parser puede aceptar un prefijo válido antes de símbolos posteriores. No uses esta función como validador de seguridad.
CommandCompiler
CommandCompiler crea un objeto llamable con una interfaz similar a compile_command().
compilador = codeop.CommandCompiler()
codigo = compilador(
"x = 1",
filename="<sesion>",
symbol="single",
)La diferencia importante es que una instancia recuerda las instrucciones __future__ compiladas previamente.
Conservar el estado de __future__
Un REPL debe mantener activas las opciones futuras del compilador para los comandos siguientes.
compilador = codeop.CommandCompiler()
ambiente = {"__name__": "__console__"}
primero = compilador(
"from __future__ import annotations",
"<sesion>",
"single",
)
exec(primero, ambiente)
segundo = compilador(
"def procesar(valor: TipoTodaviaNoDefinido): pass",
"<sesion>",
"single",
)
exec(segundo, ambiente)Crear un compilador nuevo para cada entrada perdería este estado. Mantén una instancia por sesión.
La clase Compile
Compile se comporta de forma parecida al built-in compile() y también recuerda flags futuras.
compilador = codeop.Compile()
codigo = compilador(
"resultado = 2 + 2",
"<entrada>",
"exec",
)Usa Compile cuando la aplicación ya sabe que la fuente está completa. CommandCompiler añade la detección de entrada incompleta.
codeop y el módulo code
El módulo code ofrece clases preparadas para intérpretes y consolas. La documentación oficial de code describe InteractiveInterpreter e InteractiveConsole.
Usa codeop directamente cuando un protocolo, interfaz, almacenamiento de sesión o transporte personalizado requiere control detallado. Para una consola embebida convencional, las clases de más alto nivel reducen boilerplate.
Capturar stdout y stderr
Una interfaz gráfica o remota necesita capturar la salida.
from contextlib import redirect_stdout, redirect_stderr
from io import StringIO
salida = StringIO()
with redirect_stdout(salida), redirect_stderr(salida):
exec(codigo, ambiente, ambiente)
print(salida.getvalue())La redirección global no es segura entre threads concurrentes. Ejecuta cada sesión remota en un proceso separado.
Namespaces de ejecución
Pasar el mismo diccionario como globals y locals conserva nombres entre comandos.
ambiente = {
"__name__": "__console__",
}
exec(codigo, ambiente, ambiente)Este diccionario no es una sandbox. Incluso reduciendo __builtins__, los objetos disponibles pueden ofrecer acceso al filesystem, red, imports o introspección.
exec no crea una sandbox segura
El código Python arbitrario debe considerarse equivalente a acceso al proceso. Puede leer archivos, consumir CPU y memoria, crear threads, abrir sockets y terminar el programa.
Para entrada no confiable, usa un proceso o container aislado, usuario sin privilegios, filesystem restringido, red bloqueada, límites de CPU y memoria, timeout y destrucción completa del entorno después de ejecutar.
Timeouts
Una thread no puede interrumpir de forma segura cualquier código Python o nativo. Ejecuta cada evaluación en un subprocess y termina el proceso cuando expire el plazo.
from subprocess import run, TimeoutExpired
try:
run(
["python", "runner_aislado.py"],
input=fuente,
text=True,
timeout=3,
check=True,
)
except TimeoutExpired:
print("Tiempo excedido")El runner todavía necesita límites de recursos del sistema operativo.
Sesiones concurrentes
Cada sesión debe tener su propio buffer, CommandCompiler y namespace. Nunca compartas un diccionario de globals entre usuarios.
class Sesion:
def __init__(self):
self.buffer = []
self.compilador = codeop.CommandCompiler()
self.ambiente = {"__name__": "__console__"}En aplicaciones distribuidas, almacena solo la fuente y los resultados necesarios. Los objetos Python vivos no tienen una serialización general segura.
Formatear SyntaxError
SyntaxError contiene filename, línea, offset, texto fuente y mensaje.
except SyntaxError as error:
print(error.filename, error.lineno, error.offset)
print(error.text)
print(error.msg)El módulo traceback puede generar una representación consistente. Las interfaces públicas deben ocultar rutas internas.
Auditoría de consolas administrativas
Una consola administrativa debe registrar usuario autenticado, horario, origen, duración, estado, versión del runtime y un hash de la fuente enviada. Evita almacenar secretos introducidos accidentalmente y define límites de retención.
Exige autenticación fuerte, autorización explícita y, para producción, posiblemente aprobación adicional. Un REPL remoto amplía considerablemente la superficie de ataque.
Probar la detección de completitud
casos_incompletos = [
"if True:",
"def funcion(x):",
"(",
"'''texto",
]
for fuente in casos_incompletos:
assert codeop.compile_command(fuente) is NoneAñade casos completos, inválidos, Unicode, decorators, comprehensions, match, async y sintaxis específica de la versión soportada.
Compatibilidad entre versiones
codeop utiliza el parser del intérprete en ejecución. Una entrada válida en Python 3.14 puede ser inválida en Python 3.11. Las instrucciones futuras también siguen las features de la versión.
Registra la versión de cada sesión y ejecuta la fuente en el mismo runtime usado para validarla.
Errores frecuentes
- Tratar
Nonecomo error de sintaxis. - Crear un nuevo
CommandCompilerpara cada línea. - Perder saltos de línea al construir el buffer.
- Ejecutar código de usuario en el proceso principal.
- Considerar built-ins reducidos una sandbox.
- Compartir namespace entre sesiones.
- No imponer límites de tiempo y recursos.
- Exponer tracebacks y rutas internas completas.
Buenas prácticas
- Usa un compilador por sesión.
- Trata por separado código completo, incompleto e inválido.
- Conserva líneas y filenames coherentes.
- Prefiere el módulo
codepara consolas convencionales. - Ejecuta código no confiable en proceso o container aislado.
- Aplica límites de CPU, memoria, red, filesystem y tiempo.
- Audita consolas administrativas.
- Prueba todas las categorías de sintaxis soportadas.
Conclusión
El módulo codeop en Python resuelve la parte sutil de un REPL: determinar si la entrada ya forma código completo y conservar opciones futuras del compilador entre comandos. compile_command() atiende verificaciones puntuales, mientras CommandCompiler mantiene el estado de una sesión.
La compilación es solo una etapa. Ejecutar el objeto resultante sigue siendo tan poderoso como ejecutar un script. Separando análisis y ejecución, aislando sesiones, imponiendo límites de recursos y protegiendo la interfaz, es posible construir consolas, notebooks y herramientas educativas sin convertir la comodidad en acceso irrestricto al servidor.







