Python code: Build a Custom REPL

Published on: August 12, 2026
Reading time: 5 minutes
Interactive terminal representing a custom REPL built with the Python code module

The Python code module provides the core classes for building read-eval-print loops, commonly called REPLs. With InteractiveInterpreter and InteractiveConsole, an application can embed a Python prompt, maintain a namespace, capture errors, and decide how incomplete blocks are buffered.

This is useful in teaching tools, debuggers, lightweight notebooks, administrative consoles, and local diagnostic environments. However, it executes real Python code. An embedded REPL is not a sandbox for untrusted users: imports, files, networks, processes, introspection, and objects already present in the namespace may all be reachable.

InteractiveInterpreter versus InteractiveConsole

InteractiveInterpreter handles compilation, namespace state, and execution. It does not provide prompts or multiline buffering. InteractiveConsole builds on it and adds behavior similar to the standard interpreter, including primary and continuation prompts.

from code import InteractiveConsole

console = InteractiveConsole()
console.interact(
    banner='Diagnostic console',
    exitmsg='Console closed',
)

This opens a REPL inside the current process. Every command has the same operating-system permissions as the application.

Provide a custom namespace

The locals argument accepts a mapping used as the execution namespace. It lets you expose useful objects and keep variables between commands.

from code import InteractiveConsole

namespace = {
    'status': lambda: {'queue': 4, 'health': 'ok'},
    'version': '2.1.0',
}
console = InteractiveConsole(locals=namespace)

A reduced mapping improves ergonomics but does not create a security boundary. Python introspection and reachable objects can provide indirect access to sensitive capabilities.

Compile and execute with runsource()

runsource() compiles and executes a source string. Its return value indicates whether more input is required: True means incomplete input; False means it ran or was rejected.

from code import InteractiveInterpreter

interpreter = InteractiveInterpreter()
more = interpreter.runsource('for i in range(3):')
print(more)  # True

This protocol lets a custom interface switch between a primary prompt and a continuation prompt.

Buffer blocks with push()

InteractiveConsole.push() appends a line to an internal buffer and tries to compile the complete content.

from code import InteractiveConsole

console = InteractiveConsole()
print(console.push('def double(x):'))
print(console.push('    return x * 2'))
print(console.push(''))
print(console.push('double(5)'))

When a block completes or becomes invalid, the buffer is reset. resetbuffer() discards pending input after a cancellation or interface reset.

Detect incomplete Python input

compile_command() tries to make the same complete-versus-incomplete decision as the real interpreter. It returns a code object for valid complete input, None for incomplete input, and raises SyntaxError for complete invalid input.

from code import compile_command

result = compile_command('if active:', symbol='single')
assert result is None

The guide to Python codeop covers persistent compiler flags and future statements in greater detail.

Customize input with raw_input()

Subclasses can override raw_input(). This makes it possible to read from a graphical editor, a local socket, a browser interface, or an internal queue.

class QueueConsole(InteractiveConsole):
    def __init__(self, queue, **kwargs):
        super().__init__(**kwargs)
        self.queue = queue

    def raw_input(self, prompt=''):
        display_prompt(prompt)
        return self.queue.get()

Do not expose this directly to the public internet. A remote console requires strong authentication, restricted networking, a dedicated unprivileged process, resource limits, timeouts, and complete auditing.

Capture errors by overriding write()

Syntax messages and tracebacks normally go to sys.stderr. Override write() to collect them or send them to another interface.

class CapturingConsole(InteractiveConsole):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.errors = []

    def write(self, data):
        self.errors.append(data)

Avoid returning complete tracebacks to external users because they may reveal paths, module names, configuration, and data. Keep detailed diagnostics in protected logs.

Syntax errors and runtime tracebacks

showsyntaxerror() formats compile-time problems, while showtraceback() handles runtime exceptions and removes the interpreter’s own frame. Chained exceptions have been displayed fully since Python 3.5.

You may override these methods for structured JSON or localized output, but preserve the original exception internally for debugging.

Execute compiled objects with runcode()

runcode() runs a compiled code object. Ordinary exceptions are displayed, but SystemExit may propagate. KeyboardInterrupt can also arise outside the expected point.

compiled = compile('answer = 6 * 7', '<console>', 'exec')
interpreter = InteractiveInterpreter()
interpreter.runcode(compiled)
print(interpreter.locals['answer'])

The embedding application needs a clear policy for process exit, cancellation, and cleanup.

local_exit in Python 3.13 and newer

InteractiveConsole and interact() accept local_exit=True. In this mode, exit() and quit() return from the console instead of raising SystemExit through the whole host application.

console = InteractiveConsole(local_exit=True)
console.interact()

This improves embedding, but code can still call os._exit(), alter threads, close resources, or damage the process in other ways.

Use code.interact() for convenience

code.interact() creates a temporary console. It accepts a banner, read function, namespace, exit message, and local-exit setting.

import code

state = {'order_id': 123, 'mode': 'debug'}
code.interact(
    banner='Local support session',
    local=state,
    exitmsg='',
    local_exit=True,
)

This can be placed behind a development flag. Make sure the flag is disabled in production deployments.

A REPL for local debugging

You can start a trusted console at a specific diagnostic point and expose a carefully chosen object.

def diagnose(obj):
    code.interact(
        local={'object': obj, 'summary': obj.summary},
        local_exit=True,
    )

Do not include credentials, tokens, or objects with destructive methods. Even a local terminal may be shared, recorded, or accessed through an administrative account.

Namespace persistence and memory

Variables, functions, and classes remain in the locals mapping while the interpreter instance lives. This supports natural exploration but can retain large objects and grow memory usage.

Offer a reset command, remove temporary names, or recreate the console periodically. The article on Python weakref explains object retention, although lifecycle policy should remain explicit.

Pickle limitations

Functions and classes defined in an interpreter belong to the provided namespace. They are conventionally pickleable only when that namespace corresponds to an existing importable module. Do not expect arbitrary interactive sessions to serialize cleanly.

Persist data through defined formats and reconstruct executable behavior from version-controlled source. Never unpickle untrusted input.

Real isolation requires another process

Removing __builtins__ or blocking a handful of names does not create a Python sandbox. Introspection and reachable types make such filters unreliable.

For reduced impact, run the REPL in a disposable process or container with an unprivileged user, limited filesystem, no network, CPU and memory limits, and a strict timeout. Even then, treat it as code execution rather than data entry.

Test a custom console

Test return values from push(), namespace changes, captured output, syntax errors, runtime exceptions, incomplete blocks, EOF, and local exit.

def test_console():
    console = CapturingConsole(locals={})
    assert console.push('x = 10') is False
    assert console.locals['x'] == 10
    assert console.push('for i in range(2):') is True
    console.resetbuffer()

Inject fake services and never connect unit tests to production resources.

Common mistakes

  • Treating a reduced namespace as a sandbox.
  • Exposing the REPL on a public port.
  • Leaving debug consoles enabled in production.
  • Returning sensitive tracebacks to clients.
  • Ignoring SystemExit and KeyboardInterrupt.
  • Retaining large objects indefinitely.
  • Trying to persist session functions with pickle.

Best practices

  • Use the module only for trusted operators.
  • Prefer disposable processes for isolation.
  • Control namespace and session lifetime.
  • Capture output through write().
  • Limit CPU, memory, network, and wall time.
  • Disable the feature by default in production.
  • Audit access without logging secrets.

Conclusion

The Python code module makes custom REPLs practical by providing persistent namespaces, block buffering, prompts, error display, and local exit behavior. It is a powerful base for development and diagnostic tools.

Its security meaning must be explicit: the input is executable code. For untrusted users, rely on operating-system isolation, not namespace filters. Consult the official code module documentation and the codeop documentation.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code on screen representing module and package inspection
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    inspect.ispackage: Identify Python Packages

    Learn Python inspect.ispackage to identify packages, explore module trees, and build safer introspection and plugin tools.

    Ler mais

    Tempo de leitura: 5 minutos
    05/10/2026