Python codeop: Compile Interactive Input

Published on: August 4, 2026
Reading time: 6 minutes
Programming terminal representing interactive input compilation with Python codeop

An interactive console must decide whether the text entered by a user already forms a complete Python statement. After if condition:, for example, it should display a continuation prompt instead of trying to execute immediately. The Python codeop module provides the compilation utilities used by REPLs to distinguish complete code from incomplete prefixes and to preserve __future__ statements across commands.

This guide covers compile_command(), Compile, and CommandCompiler, including error handling and the design of a controlled interactive loop. It complements our articles about bytecode with dis, scopes with symtable, tracebacks, inspect, and Python IDLE.

The problem a REPL must solve

A read-eval-print loop reads text, compiles it, executes it, and displays a result. The challenge is that a line may be complete, invalid, or merely incomplete.

if user_is_active:
    grant_access()

After the first line, the parser expects an indented suite. A console should answer with ..., collect more input, and try compilation again.

compile_command()

codeop.compile_command() attempts to compile a string as interactive input.

import codeop

result = codeop.compile_command("x = 10")
print(result)

Complete and valid source returns a code object. A valid prefix that requires more input returns None. Invalid syntax raises an exception.

Incomplete versus invalid input

inputs = [
    "x = 10",
    "if x > 0:",
    "if :",
]

for text in inputs:
    try:
        code = codeop.compile_command(text)
    except SyntaxError as error:
        print("Invalid:", error)
    else:
        if code is None:
            print("Incomplete")
        else:
            print("Complete")

This three-way distinction is the primary reason to use codeop rather than calling the built-in compile() directly.

The filename argument

The filename appears in syntax messages and later tracebacks.

code = codeop.compile_command(
    "result = 10 / 0",
    filename="<admin-console>",
)

Choose a useful identifier such as <console>, <rule-42>, or a real script path. Do not place secrets or personal data in the filename.

The symbol argument

symbol selects the compilation mode:

  • single: one interactive statement, the default;
  • exec: a sequence of statements;
  • eval: an expression.
expression = codeop.compile_command(
    "10 * 2",
    symbol="eval",
)

print(eval(expression, {}))

Any other value raises ValueError. Select the mode according to the interface instead of allowing an untrusted client to choose it freely.

single mode and result display

The single mode is designed for interactive behavior. Expression results may be sent to the interpreter’s display hook, similar to the standard console.

code = codeop.compile_command("2 + 3", symbol="single")
exec(code)

A graphical or web interface will generally capture output and define its own display representation.

Build a line accumulator

A simple loop keeps a buffer until compilation becomes complete.

import codeop

buffer = []

while True:
    prompt = "... " if buffer else ">>> "
    line = input(prompt)
    buffer.append(line)
    source = "\n".join(buffer)

    try:
        code = codeop.compile_command(source)
    except (SyntaxError, OverflowError, ValueError) as error:
        print(f"Error: {error}")
        buffer.clear()
        continue

    if code is None:
        continue

    exec(code, globals(), globals())
    buffer.clear()

This example demonstrates the mechanism, but executing arbitrary input with exec() is dangerous. A real remote console requires isolation and strict authorization.

Blank lines

In the traditional console, a blank line completes an indented block. Preserve newlines and let compile_command() decide.

if buffer and line == "":
    source = "\n".join(buffer) + "\n"

Test function and class definitions, try, with, decorators, multiline strings, and open parentheses.

Compilation errors

The helper can raise SyntaxError for invalid syntax and OverflowError or ValueError for selected literals and arguments.

try:
    code = codeop.compile_command(source)
except SyntaxError as error:
    show_syntax_error(error)
except (OverflowError, ValueError) as error:
    show_compilation_error(error)

The official codeop documentation also notes rare parser cases in which a valid prefix can be accepted before trailing symbols. Do not treat this function as a security validator.

CommandCompiler

CommandCompiler creates a callable object with an interface similar to compile_command().

compiler = codeop.CommandCompiler()

code = compiler(
    "x = 1",
    filename="<session>",
    symbol="single",
)

The important difference is that an instance remembers compiled __future__ statements.

Preserve __future__ state

A REPL must keep future compiler options enabled for subsequent commands.

compiler = codeop.CommandCompiler()
environment = {"__name__": "__console__"}

first = compiler(
    "from __future__ import annotations",
    "<session>",
    "single",
)
exec(first, environment)

second = compiler(
    "def process(value: TypeNotDefinedYet): pass",
    "<session>",
    "single",
)
exec(second, environment)

Creating a new compiler for every entry would lose this accumulated state. Keep one instance per session.

The Compile class

Compile behaves similarly to the built-in compile() and also remembers future flags.

compiler = codeop.Compile()

code = compiler(
    "result = 2 + 2",
    "<input>",
    "exec",
)

Use Compile when the application already knows the source is complete. CommandCompiler adds incomplete-input detection.

codeop and the code module

The code module provides ready-made base classes for interactive interpreters and consoles. The official code module documentation describes InteractiveInterpreter and InteractiveConsole.

Use codeop directly when a custom protocol, UI, session store, or transport requires control over compilation. For a conventional embedded console, the higher-level classes reduce boilerplate.

Capture stdout and stderr

A GUI or remote interface normally needs to capture output.

from contextlib import redirect_stdout, redirect_stderr
from io import StringIO

output = StringIO()

with redirect_stdout(output), redirect_stderr(output):
    exec(code, environment, environment)

print(output.getvalue())

Process-wide redirection is unsafe across concurrent threads. Run each remote session in a separate process.

Execution namespaces

Passing the same dictionary as globals and locals preserves names across commands.

environment = {
    "__name__": "__console__",
}

exec(code, environment, environment)

This dictionary is not a sandbox. Even if __builtins__ is reduced, available objects may expose the filesystem, network, imports, or introspection paths.

exec cannot create a safe sandbox by itself

Arbitrary Python code should be considered equivalent to process access. It can read files, consume CPU and memory, create threads, open sockets, and terminate the program.

For untrusted input, use an isolated process or container, an unprivileged operating-system user, a restricted filesystem, blocked network access, CPU and memory limits, a timeout, and complete disposal of the environment afterward.

Timeouts

A thread cannot safely interrupt every kind of Python or native code. Execute evaluations in a subprocess and terminate the process when the deadline expires.

from subprocess import run, TimeoutExpired

try:
    run(
        ["python", "isolated_runner.py"],
        input=source,
        text=True,
        timeout=3,
        check=True,
    )
except TimeoutExpired:
    print("Time limit exceeded")

The runner still needs operating-system resource restrictions.

Concurrent sessions

Every session should have its own buffer, CommandCompiler, and namespace. Never share a globals dictionary among users.

class Session:
    def __init__(self):
        self.buffer = []
        self.compiler = codeop.CommandCompiler()
        self.environment = {"__name__": "__console__"}

In distributed applications, store only necessary source and result data. Arbitrary live Python objects do not have a safe general serialization format.

Format SyntaxError

SyntaxError carries filename, line, offset, source text, and a message.

except SyntaxError as error:
    print(error.filename, error.lineno, error.offset)
    print(error.text)
    print(error.msg)

The traceback module can produce a consistent representation. Public interfaces should sanitize internal paths.

Audit administrative consoles

An administrative console should record the authenticated user, timestamp, origin, duration, status, runtime version, and a hash of the submitted source. Avoid storing secrets entered accidentally and define retention limits.

Require strong authentication, explicit authorization, and potentially an approval step for production systems. A remote REPL dramatically increases attack surface.

Test completeness detection

incomplete_cases = [
    "if True:",
    "def function(x):",
    "(",
    "'''text",
]

for source in incomplete_cases:
    assert codeop.compile_command(source) is None

Add complete, invalid, Unicode, decorator, comprehension, match, async, and release-specific syntax cases.

Version compatibility

codeop uses the parser of the running interpreter. Input valid in Python 3.14 may be invalid in Python 3.11. Future-statement behavior also follows the features available in that release.

Record the session’s Python version and execute source in the same runtime used to validate it.

Common mistakes

  • Treating None as a syntax error.
  • Creating a new CommandCompiler for every line.
  • Losing newlines while building the buffer.
  • Executing user code in the main server process.
  • Calling reduced built-ins a sandbox.
  • Sharing a namespace across sessions.
  • Omitting time and resource limits.
  • Exposing complete tracebacks and internal paths.

Best practices

  • Use one compiler per session.
  • Handle complete, incomplete, and invalid input separately.
  • Preserve line boundaries and meaningful filenames.
  • Prefer the code module for conventional consoles.
  • Run untrusted code in an isolated process or container.
  • Apply CPU, memory, network, filesystem, and time limits.
  • Audit administrative consoles.
  • Test every supported syntax category.

Conclusion

The Python codeop module solves the subtle part of a REPL: determining whether input already forms complete code and preserving future compiler options between commands. compile_command() handles one-off checks, while CommandCompiler maintains state for a session.

Compilation is only one step. Executing the resulting object remains as powerful as running a script. By separating analysis from execution, isolating sessions, enforcing resource limits, and protecting the interface, developers can build consoles, notebooks, and educational tools without turning convenience into unrestricted server access.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Developer analyzing code structure and symbol tables with Python symtable
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python symtable: Scopes and Symbols

    Learn Python symtable to analyze scopes, symbols, globals, nonlocals, closures, imports, annotations, and type parameters.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026
    Monitor with binary code representing bytecode analysis with Python dis
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python dis: Understand Bytecode

    Learn Python dis to inspect bytecode instructions, adaptive caches, source positions, tracebacks, and CPython implementation details.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026
    Error screen representing crash and deadlock diagnosis with Python faulthandler
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python faulthandler: Diagnose Crashes

    Learn Python faulthandler to diagnose crashes, deadlocks, and timeouts using thread dumps and native C stack information.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026
    Laptop with code representing Python traceback analysis and debugging
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python traceback: Errors and Call Stacks

    Learn Python traceback to capture, format, and log error call stacks safely without leaking sensitive data or retaining memory.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026
    Software analysis representing object introspection with Python inspect
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python inspect: Object Introspection

    Learn Python inspect to analyze functions, classes, signatures, source code, decorators, generators, coroutines, and frames safely.

    Ler mais

    Tempo de leitura: 6 minutos
    02/08/2026
    Memory module representing weak references and caches in Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python weakref: Weak References

    Learn Python weakref to build weak references, automatic caches, observer registries, and finalizers without retaining objects in memory.

    Ler mais

    Tempo de leitura: 8 minutos
    28/07/2026