2025, Nov 01 17:00

How to Generate a REPL-Style Transcript from a Python File Using InteractiveConsole

Turn any .py file into a true Python REPL transcript. Drive code.InteractiveConsole, print >>> and ... prompts, and capture stdout for repeatable output.

Running a Python file rarely feels like a real REPL session. In script mode the interpreter skips prompts, doesn’t echo expression results, and swallows the interactive flow that many developers and tools rely on. Sometimes you need the opposite: feed a file to Python and get output that looks exactly like a manual interactive session, including >>> and ... prompts and expression echoes.

Example input that should behave like a REPL session

Consider a plain Python file you want to replay as if it were typed line by line in the interactive shell:

n = 5
n + 8
if n < 4:
    print("123")
else:
    print("xyz")
exit()

What’s really happening and why it’s tricky

The interpreter has two distinct modes. In interactive/REPL mode it prints prompts, evaluates and displays the value of bare expressions, and handles multi-line statements with continuation prompts. In script mode it simply executes the file without prompts and without echoing expression values. Standard command-line options do not flip a script into a true REPL transcript.

There is a straightforward way to simulate the interactive behavior programmatically. The code module exposes an InteractiveConsole that accepts code line by line and behaves like the REPL does for evaluation. By feeding your file into that console, printing prompts yourself, and capturing stdout, you can generate the transcript you expect. If you are wondering whether doctest does something similar, it runs example snippets with exec(compile(example.source)), which is not a full REPL transcript generator.

The fix: drive code.InteractiveConsole and capture stdout

The approach is to read the file, stream each line into an InteractiveConsole, render >>> and ... prompts on your own, and capture whatever the interpreter prints. The snippet below also understands lines starting with ... so you can include continuation markers in the source if you want to mirror interactive input formatting for multi-line blocks.

import code
import sys
import io
from contextlib import redirect_stdout
def emulate_repl_session(path_to_file):
    # Read the file into memory
    with open(path_to_file, 'r') as fh:
        rows = fh.readlines()
    # Normalize line endings and keep blank-line handling predictable
    rows = [r.rstrip('\n') for r in rows]
    # REPL banner (optional to print or skip)
    print(f"Python {sys.version} on {sys.platform}")
    print('Type "help", "copyright", "credits" or "license" for more information.')
    # Create an interactive console
    interp = code.InteractiveConsole()
    # Buffers and state for multi-line input
    chunk = []
    awaiting = False
    for row in rows:
        # Ignore empty lines to match typical REPL typing
        if not row.strip():
            continue
        # Continuation lines that start with '...'
        if row.strip().startswith('...'):
            chunk.append(row.replace('...', '', 1).lstrip())
            continue
        # If we were gathering a multi-line block, flush and execute it
        if awaiting:
            suite = '\n'.join(chunk)
            print(f'>>> {chunk[0]}')
            for frag in chunk[1:]:
                print(f'... {frag}')
            with io.StringIO() as stream, redirect_stdout(stream):
                cont = interp.push(suite)
                emitted = stream.getvalue()
            if emitted:
                print(emitted, end='')
            chunk = []
            awaiting = cont
            if row.strip() == 'exit()':
                break
            if not awaiting:
                continue
        # Handle explicit session exit
        if row.strip() == 'exit()':
            print('>>> exit()')
            break
        # Print prompt and the input line
        print(f'>>> {row}')
        # Start collecting a block if the line opens a suite
        if row.rstrip().endswith(':'):
            chunk.append(row)
            awaiting = True
            continue
        # Execute a single line and capture what it prints
        with io.StringIO() as stream, redirect_stdout(stream):
            cont = interp.push(row)
            emitted = stream.getvalue()
        if emitted:
            print(emitted, end='')
        awaiting = cont
if __name__ == '__main__':
    if len(sys.argv) != 2:
        print('Usage: python repl_emulator.py <script_path>')
        sys.exit(1)
    emulate_repl_session(sys.argv[1])

Why this works

InteractiveConsole executes code fragments the same way the REPL does, including evaluation semantics that cause expressions to produce visible output. By controlling prompts and forwarding each fragment through push, you re-create the user-facing transcript. stdout is captured with contextlib.redirect_stdout and then printed verbatim, so what the interpreter emits appears exactly where a human would expect it in a live session.

Why it’s worth knowing

Reproducible REPL transcripts can make automation, demonstration, or verification much simpler. Instead of copy-pasting from a live terminal, you can keep a plain .py file and regenerate the same interactive-looking output whenever you need to present behavior clearly and consistently. If your input already contains continuation lines beginning with ..., the flow remains natural and readable while still being deterministic.

Wrap-up

When you need script execution to look like an actual interactive session, lean on code.InteractiveConsole and a tiny harness around it. Feed the file line by line, print prompts, capture stdout, and let the interpreter do the rest. Keep the source lightweight, prefer plain Python statements rather than pre-inserting >>> markers, and use ... only when you intentionally mirror multi-line entry. This gives you a faithful REPL-style transcript without fighting command-line flags or reinventing evaluation semantics.

The article is based on a question from StackOverflow by Thomas Weise and an answer by Tushar Neupaney.