Python’s codeop module provides the incomplete-input detector used by code.InteractiveConsole — returning None when more input is needed, a code object when the statement is complete, or raising SyntaxError on bad input. import codeop. compile_command: codeop.compile_command(source, filename="<input>", symbol="single") → code object if complete; None if incomplete (needs more lines); raises SyntaxError / OverflowError / ValueError on bad syntax. symbol: "single" (interactive, prints expression values), "exec" (module), "eval" (expression). CommandCompiler: cc = codeop.CommandCompiler() — stateful compiler; call cc(source, filename, symbol) — same return contract but accumulates __future__ imports (e.g. from __future__ import annotations) so they apply to all subsequent compiles in the session. The key use pattern: send each input line to compile_command; if None, prompt for continuation; if a code object, exec it; if SyntaxError, display the error. Works incrementally: "if True:" → None; "if True:\n pass" → code object. Claude Code generates REPL harnesses, notebook kernels, multi-line shell input handlers, and incremental syntax validators.
CLAUDE.md for codeop
## codeop Stack
- Stdlib: import codeop
- One-shot: code = codeop.compile_command(src, "<input>", "single")
# None = need more; code object = complete; SyntaxError = bad
- Stateful: cc = codeop.CommandCompiler()
code = cc(src, "<repl>", "single") # tracks __future__ imports
- Loop: buf = []
while True:
line = input("... " if buf else ">>> ")
buf.append(line)
code = codeop.compile_command("\n".join(buf))
if code: exec(code); buf = []
elif code is None: continue # incomplete
else: buf = [] # SyntaxError printed
codeop Interactive Input Pipeline
# app/codeoputil.py — compile, REPL loop, multi-line buffer, batch runner, validator
from __future__ import annotations
import codeop
import sys
import traceback
import types
from dataclasses import dataclass, field
from io import StringIO
from typing import Any, Callable
# ─────────────────────────────────────────────────────────────────────────────
# 1. Low-level compile helpers
# ─────────────────────────────────────────────────────────────────────────────
class CompileResult:
"""Discriminated result of an attempted compile."""
__slots__ = ("code", "incomplete", "error", "error_msg")
def __init__(
self,
code: types.CodeType | None = None,
incomplete: bool = False,
error: Exception | None = None,
) -> None:
self.code = code
self.incomplete = incomplete
self.error = error
self.error_msg = str(error) if error else ""
@property
def ok(self) -> bool:
return self.code is not None
def __repr__(self) -> str:
if self.ok: return f"<CompileResult: complete>"
if self.incomplete: return f"<CompileResult: incomplete>"
return f"<CompileResult: error: {self.error_msg}>"
def try_compile(
source: str,
filename: str = "<input>",
symbol: str = "single",
) -> CompileResult:
"""
Attempt to compile source. Returns a CompileResult discriminating between
complete code, incomplete input (needs more lines), and syntax errors.
Example:
r = try_compile("if True:")
print(r.incomplete) # True — more lines needed
r = try_compile("if True:\n pass")
print(r.ok) # True — complete
"""
try:
code = codeop.compile_command(source, filename, symbol)
if code is None:
return CompileResult(incomplete=True)
return CompileResult(code=code)
except (SyntaxError, OverflowError, ValueError) as e:
return CompileResult(error=e)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Multi-line input buffer
# ─────────────────────────────────────────────────────────────────────────────
class InputBuffer:
"""
Accumulates lines of input and detects when a complete Python statement
has been entered.
Example:
buf = InputBuffer()
buf.push("def add(a, b):") # → incomplete
buf.push(" return a + b") # → incomplete
buf.push("") # blank line → complete
code = buf.code_object # ready to exec
"""
def __init__(
self,
filename: str = "<input>",
symbol: str = "single",
compiler: codeop.CommandCompiler | None = None,
) -> None:
self._filename = filename
self._symbol = symbol
self._compiler = compiler or codeop.CommandCompiler()
self._lines: list[str] = []
self._code: types.CodeType | None = None
self._error: Exception | None = None
self._complete = False
def push(self, line: str) -> bool:
"""
Add a line. Returns True if the buffer is now complete (ready to exec).
Raises SyntaxError (stored in .error) on bad input.
"""
self._lines.append(line)
source = "\n".join(self._lines)
try:
code = self._compiler(source, self._filename, self._symbol)
except (SyntaxError, OverflowError, ValueError) as e:
self._error = e
self._lines.clear()
self._complete = False
return False
if code is None:
self._complete = False
return False
else:
self._code = code
self._complete = True
return True
def reset(self) -> None:
self._lines.clear()
self._code = None
self._error = None
self._complete = False
@property
def is_complete(self) -> bool:
return self._complete
@property
def is_empty(self) -> bool:
return len(self._lines) == 0
@property
def source(self) -> str:
return "\n".join(self._lines)
@property
def code_object(self) -> types.CodeType | None:
return self._code
@property
def error(self) -> Exception | None:
return self._error
@property
def prompt(self) -> str:
return "... " if self._lines else ">>> "
# ─────────────────────────────────────────────────────────────────────────────
# 3. REPL executor
# ─────────────────────────────────────────────────────────────────────────────
class ReplExecutor:
"""
Execute code objects in a shared namespace with output capture.
Example:
repl = ReplExecutor()
code = codeop.compile_command("x = 42", "<r>", "single")
repl.run(code)
code2 = codeop.compile_command("print(x)", "<r>", "single")
out, err = repl.run_capture(code2)
print(out) # "42\n"
"""
def __init__(self, namespace: dict[str, Any] | None = None) -> None:
self.namespace: dict[str, Any] = namespace if namespace is not None else {
"__name__": "__console__",
"__doc__": None,
}
def run(self, code: types.CodeType) -> None:
"""Execute code in the shared namespace, printing exceptions."""
try:
exec(code, self.namespace)
except SystemExit:
raise
except Exception:
traceback.print_exc()
def run_capture(
self, code: types.CodeType
) -> tuple[str, str]:
"""Execute code and capture stdout/stderr. Returns (stdout, stderr)."""
old_out, old_err = sys.stdout, sys.stderr
sys.stdout = buf_out = StringIO()
sys.stderr = buf_err = StringIO()
try:
exec(code, self.namespace)
except SystemExit:
raise
except Exception:
traceback.print_exc(file=sys.stderr)
finally:
sys.stdout, sys.stderr = old_out, old_err
return buf_out.getvalue(), buf_err.getvalue()
def get(self, name: str) -> Any:
return self.namespace.get(name)
def set(self, name: str, value: Any) -> None:
self.namespace[name] = value
# ─────────────────────────────────────────────────────────────────────────────
# 4. Batch source runner (split on complete statements)
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class BatchResult:
statements: int
errors: list[tuple[int, str]] = field(default_factory=list) # (stmt_idx, msg)
outputs: list[str] = field(default_factory=list)
@property
def success(self) -> bool:
return len(self.errors) == 0
def __str__(self) -> str:
return (f"statements={self.statements} "
f"errors={len(self.errors)} "
f"output_bytes={sum(len(o) for o in self.outputs)}")
def run_batch(
source: str,
namespace: dict[str, Any] | None = None,
capture: bool = True,
) -> BatchResult:
"""
Compile and execute multi-statement source, splitting it into individual
complete statements using codeop.
Example:
result = run_batch("x = 1\\ny = x + 1\\nprint(y)", capture=True)
print(result.outputs) # ["2\\n"]
"""
executor = ReplExecutor(namespace)
buf = InputBuffer(filename="<batch>", symbol="exec")
result = BatchResult(statements=0)
stmt_idx = 0
for line in (source + "\n").splitlines():
complete = buf.push(line)
if buf.error:
result.errors.append((stmt_idx, str(buf.error)))
buf.reset()
continue
if complete and buf.code_object:
if capture:
out, err = executor.run_capture(buf.code_object)
if out:
result.outputs.append(out)
if err:
result.errors.append((stmt_idx, err.strip()))
else:
executor.run(buf.code_object)
result.statements += 1
stmt_idx += 1
buf.reset()
# flush any remaining incomplete buffer
if not buf.is_empty:
# try adding blank line to flush compound statement
buf.push("")
if buf.is_complete and buf.code_object:
if capture:
out, err = executor.run_capture(buf.code_object)
if out:
result.outputs.append(out)
else:
executor.run(buf.code_object)
result.statements += 1
return result
# ─────────────────────────────────────────────────────────────────────────────
# 5. Syntax validator
# ─────────────────────────────────────────────────────────────────────────────
def validate_source(source: str, filename: str = "<string>") -> list[str]:
"""
Check a multi-line source string for syntax errors using codeop.
Returns a list of error messages (empty if valid).
Example:
errors = validate_source("def bad(\\n # missing close")
print(errors) # ["unexpected EOF while parsing ..."]
"""
errors: list[str] = []
buf = InputBuffer(filename=filename, symbol="exec")
for line in source.splitlines():
buf.push(line)
if buf.error:
errors.append(str(buf.error))
buf.reset()
# Final flush
if not buf.is_empty:
buf.push("")
if buf.error:
errors.append(str(buf.error))
return errors
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== codeop demo ===")
# ── try_compile ────────────────────────────────────────────────────────────
print("\n--- try_compile ---")
cases = [
("x = 1 + 2", "single"),
("if True:", "single"),
("if True:\n pass", "single"),
("def f(\n", "single"),
("1 +", "single"),
("import os; os.x(??)", "single"),
]
for src, sym in cases:
r = try_compile(src, symbol=sym)
print(f" {src[:35]!r:38s} → {r!r}")
# ── InputBuffer ────────────────────────────────────────────────────────────
print("\n--- InputBuffer multi-line ---")
ibuf = InputBuffer()
lines = [
"def greet(name: str) -> str:",
" return f'Hello, {name}!'",
"", # blank line completes the function
]
for line in lines:
complete = ibuf.push(line)
print(f" push({line!r:35s}) → complete={complete} prompt={ibuf.prompt!r}")
if ibuf.code_object:
print(f" code object: {ibuf.code_object}")
# ── ReplExecutor ───────────────────────────────────────────────────────────
print("\n--- ReplExecutor ---")
repl = ReplExecutor()
snippets = [
"x = 10",
"y = x * x",
"print(f'y = {y}')",
"from __future__ import annotations",
]
for snippet in snippets:
code = codeop.compile_command(snippet, "<repl>", "single")
if code:
out, err = repl.run_capture(code)
if out:
print(f" output: {out.rstrip()!r}")
print(f" x={repl.get('x')} y={repl.get('y')}")
# ── run_batch ──────────────────────────────────────────────────────────────
print("\n--- run_batch ---")
batch_src = """
import math
def circle_area(r):
return math.pi * r ** 2
for r in [1, 2, 3]:
print(f"r={r} area={circle_area(r):.3f}")
"""
result = run_batch(batch_src.strip(), capture=True)
print(f" {result}")
for out in result.outputs:
for line in out.splitlines():
print(f" {line}")
# ── validate_source ────────────────────────────────────────────────────────
print("\n--- validate_source ---")
valid_src = "x = 1\ny = x + 1\n"
invalid_src = "def bad(\n # missing close"
print(f" valid errors: {validate_source(valid_src)}")
errs = validate_source(invalid_src)
print(f" invalid errors: {[e[:60] for e in errs]}")
print("\n=== done ===")
For the code.InteractiveConsole / code.InteractiveInterpreter alternative — code.InteractiveConsole is a higher-level REPL that uses codeop.CommandCompiler internally; interact() runs a full read-eval-print loop using readline if available — use code.InteractiveConsole when you want a batteries-included REPL with banner, interact() loop, and push() API; use codeop directly when you need fine-grained control over input buffering and compilation outside of a standard console loop, such as in a web notebook kernel, a socket-based REPL, or a testing harness where you want to drive execution line-by-line and capture output after each statement. For the ast.parse / compile alternative — ast.parse(source, mode="exec") raises SyntaxError on bad input but does not distinguish between incomplete and invalid source the way codeop.compile_command() does (returning None for incomplete); compile(source, filename, mode) similarly raises SyntaxError without the incomplete-input contract — use codeop when you specifically need the three-way incomplete/complete/error discrimination for an interactive input loop; use ast.parse() when you need AST-level access to the parsed tree for static analysis or transformation. The Claude Skills 360 bundle includes codeop skill sets covering CompileResult with try_compile() one-shot discriminating compiler, InputBuffer multi-line accumulator with is_complete/prompt/code_object, ReplExecutor shared-namespace executor with run()/run_capture(), BatchResult with run_batch() multi-statement source runner, and validate_source() incremental syntax validator. Start with the free tier to try REPL construction patterns and codeop pipeline code generation.