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 NoneAdd 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
Noneas a syntax error. - Creating a new
CommandCompilerfor 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
codemodule 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.







