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

    CSV data analysis with Python csv.QUOTE_STRINGS
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    csv.QUOTE_STRINGS: Preserve Types in CSV Files

    Learn Python csv.QUOTE_STRINGS to quote text, preserve types, and build more predictable and secure CSV files.

    Ler mais

    Tempo de leitura: 4 minutos
    15/09/2026
    Code and file paths for Python PurePath.full_match
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    PurePath.full_match: Validate Paths with Glob Patterns

    Learn Python PurePath.full_match to validate complete paths with glob patterns, control case sensitivity, and build precise file filters.

    Ler mais

    Tempo de leitura: 4 minutos
    15/09/2026
    Asynchronous Python code representing asyncio.eager_task_factory
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    asyncio.eager_task_factory: Reduce Task Overhead

    Learn Python asyncio.eager_task_factory to reduce scheduling overhead, understand ordering changes, and optimize short coroutines safely.

    Ler mais

    Tempo de leitura: 4 minutos
    14/09/2026
    Developer working with UTC timestamps and Python calendar.timegm
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    calendar.timegm: Convert UTC to Unix Timestamps

    Learn Python calendar.timegm to convert UTC date tuples into Unix timestamps safely and avoid local-time conversion bugs.

    Ler mais

    Tempo de leitura: 5 minutos
    14/09/2026
    Programmer analyzing code to identify file MIME types with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    mimetypes.guess_file_type: Detect MIME Types

    Learn Python mimetypes.guess_file_type for MIME detection in paths, URLs, uploads, and HTTP responses with safe fallbacks.

    Ler mais

    Tempo de leitura: 5 minutos
    13/09/2026
    Server components representing isolated Python interpreters running in parallel
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    InterpreterPoolExecutor: True Parallelism in Python

    Learn Python InterpreterPoolExecutor for CPU-bound tasks, isolated interpreters, true parallelism, and safer concurrency design.

    Ler mais

    Tempo de leitura: 6 minutos
    13/09/2026