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

    Web application code representing WSGI with Python wsgiref
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python wsgiref: WSGI Applications

    Learn Python wsgiref to build and validate WSGI applications, test environ and headers, route requests, and run a local reference

    Ler mais

    Tempo de leitura: 4 minutos
    12/08/2026
    Secure Internet protocol representing Unicode preparation with Python stringprep
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python stringprep: Prepare Unicode

    Learn Python stringprep to apply RFC 3454 tables, map Unicode, reject prohibited characters, and validate bidirectional protocol rules.

    Ler mais

    Tempo de leitura: 5 minutos
    12/08/2026
    Network connections representing non-blocking I/O with Python selectors
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python selectors: Non-Blocking I/O

    Learn Python selectors to monitor many sockets, read and write readiness, timeouts, partial messages, and non-blocking connections safely.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Network data flow representing asynchronous context with Python contextvars
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python contextvars: Async Context State

    Learn Python contextvars to store task-local state, prevent asyncio leaks, copy contexts, propagate metadata, and restore values with tokens.

    Ler mais

    Tempo de leitura: 4 minutos
    11/08/2026
    Programming code representing operations as functions with Python operator
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python operator: Operations as Functions

    Learn Python operator to use operations as functions, sort fields, access items, call methods, and build clear functional pipelines.

    Ler mais

    Tempo de leitura: 4 minutos
    11/08/2026
    Three-dimensional alphabet representing Unicode normalization with Python unicodedata
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python unicodedata: Normalize Unicode

    Learn Python unicodedata to normalize Unicode, inspect names, categories, numeric values, combining marks, bidirectional classes, and width.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026