Skip to main content

Python Understanding the REPL Practice Problems & Exercises

Practice: Understanding the REPL

10 problems3 Easy4 Medium3 Hard30–45 min
← Back to lesson

Easy

#1Expression vs Statement — Predict the OutputEasy
REPLexpressionstatementprint

The REPL behaves differently for expressions and statements. Without running the code, predict what the REPL would display for each of the following inputs entered one at a time:

  1. 3 + 39
  2. import math
  3. y = 100

Write a script that simulates and explains these REPL behaviors by printing what the REPL would show (or indicating when nothing is shown).

Python
# Simulate what the REPL displays for each input

# 1. A bare expression: 3 + 39
print("Expression outputs:")
print(3 + 39)

# 2. A statement: import math
print("Statement outputs:")
import math
print("(nothing)")

# 3. An assignment: y = 100
print("Assignment outputs:")
y = 100
print("(nothing)")
Solution
# Simulate what the REPL displays for each input

# 1. A bare expression: 3 + 39
print("Expression outputs:")
print(3 + 39)

# 2. A statement: import math
print("Statement outputs:")
import math
print("(nothing)")

# 3. An assignment: y = 100
print("Assignment outputs:")
y = 100
print("(nothing)")

Why this matters:

The REPL uses sys.displayhook to automatically display the result of any expression you type. But not everything is an expression:

  • Expressions produce a value: 3 + 39, len([1,2,3]), "hello".upper(). The REPL calls repr() on the result and prints it.
  • Statements perform actions but do not produce a value: import math, x = 10, del x. The REPL shows nothing.
  • Assignment (y = 100) is a statement in Python, not an expression. Unlike some languages (C, JavaScript), assignment does not return a value, so the REPL stays silent.

This is why beginners sometimes think their code "did nothing" when they assign a variable — the REPL simply has nothing to display.

Expected Output
Expression outputs:\n42\nStatement outputs:\n(nothing)\nAssignment outputs:\n(nothing)
Hints

Hint 1: In the REPL, typing a bare expression like `42` causes the REPL to display its value. Statements like `x = 10` produce no visible output because they do not return a value.

Hint 2: The key distinction: expressions evaluate to a value (and the REPL auto-displays it), while statements perform an action without producing a displayable result.

#2print() vs Bare Expression — When Does the REPL Show Output?Easy
REPLprintreprNone

Explain the difference between what the REPL displays for a bare expression versus a print() call. Demonstrate with strings and None.

Python
# Demonstrate the difference between REPL auto-display and print()

text = "hello world"

# In the REPL, typing just: "hello world"
# would display the repr() of the string
print(f"Bare expression in REPL would show: {repr(text)}")

# In the REPL, typing: print("hello world")
# would display the str() of the string
print(f"print() in REPL would show: {text}")

print("---")

# None is special: REPL suppresses it
print(f"Bare None expression in REPL would show: (nothing)")
print(f"print(None) in REPL would show: {None}")

print("---")

# Show the actual difference between repr and str
print(f"Bare string expression repr: {repr(text)}")
print(f"print() string output: {str(text)}")
Solution
text = "hello world"

# In the REPL, typing just: "hello world"
# would display the repr() of the string
print(f"Bare expression in REPL would show: {repr(text)}")

# In the REPL, typing: print("hello world")
# would display the str() of the string
print(f"print() in REPL would show: {text}")

print("---")

# None is special: REPL suppresses it
print(f"Bare None expression in REPL would show: (nothing)")
print(f"print(None) in REPL would show: {None}")

print("---")

# Show the actual difference between repr and str
print(f"Bare string expression repr: {repr(text)}")
print(f"print() string output: {str(text)}")

The two display mechanisms:

  1. REPL auto-display (bare expressions): calls sys.displayhook(), which internally calls repr() on the result and prints it. If the result is None, it prints nothing at all.

  2. print() function: calls str() on each argument and writes to sys.stdout. It always prints, even None (as the literal string "None").

Practical differences for strings:

  • REPL auto-display: 'hello world' (with quotes — that is repr())
  • print(): hello world (no quotes — that is str())

Why None is suppressed: Almost every function call in the REPL returns None (e.g., print() itself returns None). If the REPL displayed None every time, the output would be cluttered with noise. The REPL intentionally filters it out.

Expected Output
Bare expression in REPL would show: 'hello world'\nprint() in REPL would show: hello world\n---\nBare None expression in REPL would show: (nothing)\nprint(None) in REPL would show: None\n---\nBare string expression repr: 'hello world'\nprint() string output: hello world
Hints

Hint 1: When the REPL auto-displays an expression, it calls `repr()` on the value. When you use `print()`, it calls `str()` instead. For strings, `repr()` adds quotes around the value while `str()` does not.

Hint 2: The REPL suppresses display of `None` — if an expression evaluates to `None`, nothing is shown. But `print(None)` explicitly prints the word "None" because `print()` converts its argument via `str()`.

#3The Underscore Variable — Last Expression RecallEasy
REPLunderscore_expression-history

Simulate the REPL's special _ (underscore) variable behavior. Show how _ gets updated after bare expressions but NOT after assignments or print() calls.

Python
import builtins

# Simulate REPL underscore behavior
def simulate_repl_display(expr_result, description):
    """Simulates how the REPL handles expression results."""
    if expr_result is not None:
        builtins._ = expr_result
        return repr(expr_result)
    return "(suppressed — None)"

# Step 1: bare expression 6 * 7
result = 6 * 7
simulate_repl_display(result, "6 * 7")
print(f"Step 1 — evaluate 6 * 7: {builtins._}")

# Step 2: check _ value
print(f"Step 2 — _ holds last result: {builtins._}")

# Step 3: use _ in next expression
result = builtins._ + 42
simulate_repl_display(result, "_ + 42")
print(f"Step 3 — use _ in next expression: {builtins._}")

# Step 4: _ is now updated
print(f"Step 4 — _ now updated: {builtins._}")

# Step 5: assignment does NOT update _
x = 999  # This is a statement, not a displayed expression
print(f"Step 5 — after assignment, _ unchanged: {builtins._}")

# Step 6: print() returns None, which is suppressed — _ unchanged
print_result = print  # print() returns None
# Calling print("hi") in REPL: "hi" is printed, but return value is None → suppressed
print(f"Step 6 — after print(), _ unchanged: {builtins._}")
Solution
import builtins

def simulate_repl_display(expr_result, description):
"""Simulates how the REPL handles expression results."""
if expr_result is not None:
builtins._ = expr_result
return repr(expr_result)
return "(suppressed — None)"

# Step 1: bare expression 6 * 7
result = 6 * 7
simulate_repl_display(result, "6 * 7")
print(f"Step 1 — evaluate 6 * 7: {builtins._}")

# Step 2: check _ value
print(f"Step 2 — _ holds last result: {builtins._}")

# Step 3: use _ in next expression
result = builtins._ + 42
simulate_repl_display(result, "_ + 42")
print(f"Step 3 — use _ in next expression: {builtins._}")

# Step 4: _ is now updated
print(f"Step 4 — _ now updated: {builtins._}")

# Step 5: assignment does NOT update _
x = 999
print(f"Step 5 — after assignment, _ unchanged: {builtins._}")

# Step 6: print() returns None — suppressed, _ unchanged
print(f"Step 6 — after print(), _ unchanged: {builtins._}")

How _ works internally:

The REPL's display hook (sys.displayhook) does three things when an expression result is not None:

  1. Stores the result in builtins._
  2. Calls repr() on the result
  3. Prints the repr to stdout

This means _ is ONLY updated when:

  • You type a bare expression (not an assignment)
  • The expression result is not None

Common pitfalls:

  • x = 42 does NOT update _ (assignment is a statement)
  • print("hi") does NOT update _ (print returns None, which is suppressed)
  • len([1,2,3]) DOES update _ (it returns 3, which is displayed)

The _ variable is stored in the builtins module so it is accessible from any scope within the REPL session.

Expected Output
Step 1 — evaluate 6 * 7: 42\nStep 2 — _ holds last result: 42\nStep 3 — use _ in next expression: 84\nStep 4 — _ now updated: 84\nStep 5 — after assignment, _ unchanged: 84\nStep 6 — after print(), _ unchanged: 84
Hints

Hint 1: In the REPL, the special variable `_` always holds the result of the last expression that was auto-displayed. Assignments and print() calls do NOT update `_` because assignments are statements and print() returns None (which is suppressed).

Hint 2: You can use `_` in subsequent expressions like any other variable. Each time a new expression result is displayed, `_` is updated to that new value.


Medium

#4REPL vs Script — Expression Display DifferencesMedium
REPLscriptdisplayhookexecution-model

Demonstrate the fundamental difference between running Python code as a script versus in the REPL. Use compile() with different modes to show why bare expressions produce output in the REPL but are silently discarded in scripts.

Python
import sys

def run_as_script(code_string):
    """Execute code as if it were a .py script file."""
    compiled = compile(code_string, "<script>", "exec")
    exec(compiled)

def run_as_repl(code_lines):
    """Execute each line as if typed into the REPL."""
    namespace = {}
    for line in code_lines:
        line = line.strip()
        if not line:
            continue
        compiled = compile(line, "<stdin>", "single")
        exec(compiled, namespace)

# The same code, two different behaviors
code = '''\
print("hello from print")
42
"world"
[x**2 for x in range(1, 4)]
'''

print("=== Running as SCRIPT ===")
run_as_script(code)

print("\n=== Running as REPL ===")
lines = ["print('hello from print')", "42", "'world'", "[x**2 for x in range(1, 4)]"]
run_as_repl(lines)
Solution
import sys

def run_as_script(code_string):
"""Execute code as if it were a .py script file."""
compiled = compile(code_string, "<script>", "exec")
exec(compiled)

def run_as_repl(code_lines):
"""Execute each line as if typed into the REPL."""
namespace = {}
for line in code_lines:
line = line.strip()
if not line:
continue
compiled = compile(line, "<stdin>", "single")
exec(compiled, namespace)

code = '''\
print("hello from print")
42
"world"
[x**2 for x in range(1, 4)]
'''

print("=== Running as SCRIPT ===")
run_as_script(code)

print("\n=== Running as REPL ===")
lines = ["print('hello from print')", "42", "'world'", "[x**2 for x in range(1, 4)]"]
run_as_repl(lines)

The mechanism behind the difference:

Python's compile() function has three modes:

ModePurposeExpression behavior
"exec"Full module/scriptExpressions are evaluated but results are discarded
"single"One interactive statementExpression results are sent to sys.displayhook()
"eval"Single expressionReturns the value (used by eval())

When you run a .py file, Python compiles the entire file with mode="exec". In this mode, bare expressions like 42 are evaluated (side effects still happen) but the result goes nowhere.

When you type into the REPL, each input is compiled with mode="single". The compiled bytecode includes a PRINT_EXPR instruction that calls sys.displayhook() on the result. This is why 42 appears as output in the REPL but not when running a script.

This is not magic — it is a compile-time decision baked into the bytecode.

import sys
import io
import textwrap

def run_as_script(code_string):
    """Execute code as if it were a .py script file (exec mode)."""
    # TODO: Use compile() + exec() with mode='exec'
    pass

def run_as_repl(code_lines):
    """Execute each line as if typed into the REPL (single mode)."""
    # TODO: Use compile() + exec() with mode='single' for each line
    pass
Expected Output
=== Running as SCRIPT ===\nhello from print\n\n=== Running as REPL ===\nhello from print\n42\n'world'\n[1, 4, 9]
Hints

Hint 1: Python's `compile()` function accepts a `mode` parameter: `"exec"` compiles a full module (like a script), while `"single"` compiles a single interactive statement (like one REPL input). In `"single"` mode, expression results are passed to `sys.displayhook`.

Hint 2: When using `mode="single"`, compile and exec each line separately to mimic how the REPL processes one input at a time. Expression statements will automatically display their result.

#5Build a Custom REPL with the code ModuleMedium
REPLcode-moduleInteractiveConsolecustom-REPL

Use Python's built-in code module to create a custom REPL. Feed it a sequence of commands programmatically and observe the output. This is how IDEs and notebooks implement their own Python shells.

Python
import code
import sys

# Create a shared namespace for our custom REPL
namespace = {}
console = code.InteractiveConsole(locals=namespace)

# Feed commands to the console
commands = [
    "x = 10",
    "x * 5",
    "y = x + 20",
    "y",
    "import math",
    "math.pi",
]

for cmd in commands:
    print(f"Pushing: {cmd}")
    # push() returns True if more input is needed (incomplete block)
    # Returns False if the command was complete
    more_input_needed = console.push(cmd)

# Inspect what ended up in the namespace
user_vars = [k for k in namespace if not k.startswith("_")]
print(f"All variables in console namespace: {', '.join(sorted(user_vars))}")
Solution
import code
import sys

namespace = {}
console = code.InteractiveConsole(locals=namespace)

commands = [
"x = 10",
"x * 5",
"y = x + 20",
"y",
"import math",
"math.pi",
]

for cmd in commands:
print(f"Pushing: {cmd}")
more_input_needed = console.push(cmd)

user_vars = [k for k in namespace if not k.startswith("_")]
print(f"All variables in console namespace: {', '.join(sorted(user_vars))}")

What the code module provides:

The code module is part of Python's standard library and contains the actual machinery behind the interactive interpreter:

  • code.InteractiveConsole: A full REPL implementation. It handles multi-line statements, compiles with "single" mode, catches exceptions, and calls sys.displayhook for expression results.
  • console.push(line): Feeds one line to the interpreter. Returns True if the line is part of an incomplete construct (like the first line of a for loop), False when the statement is complete and has been executed.
  • console.interact(): Starts a full interactive loop (reads from stdin). Not suitable for programmatic use but useful for embedding a debug shell in your application.

Real-world uses:

  • Jupyter notebooks use a similar approach (via IPython.core) to execute cells
  • IDEs like PyCharm embed a console using these primitives
  • Debugging tools like code.interact(local=locals()) drop you into a REPL mid-execution
import code
import sys

def build_custom_repl():
    """Create a custom REPL using the code module.
    Feed it a sequence of commands and capture the output.
    """
    # TODO: Use code.InteractiveConsole to push lines
    pass
Expected Output
Pushing: x = 10\nPushing: x * 5\n50\nPushing: y = x + 20\nPushing: y\n30\nPushing: import math\nPushing: math.pi\n3.141592653589793\nAll variables in console namespace: math, x, y
Hints

Hint 1: The `code.InteractiveConsole` class provides a `push()` method that feeds a line of source text to the interpreter. It returns `True` if more input is expected (e.g., an incomplete block) and `False` if the line was complete.

Hint 2: Create an `InteractiveConsole` with a shared `locals` dict. After pushing all lines, inspect that dict to see what variables were defined during the session.

#6Introspection with help() and dir()Medium
REPLintrospectionhelpdirinspect

One of the REPL's superpowers is introspection — examining objects at runtime. Build a utility that demonstrates the key introspection patterns that experienced developers use daily in the REPL.

Python
import inspect

# === dir() exploration ===
print("=== dir() exploration ===")
# Filter out dunder methods to see the public API
public_methods = [m for m in dir(str) if not m.startswith("_")]
print(f"Public methods on str: {', '.join(public_methods)}")

# === Signature inspection ===
print("\n=== Signature inspection ===")
print(f"str.split signature: {inspect.signature(str.split)}")
print(f"str.replace signature: {inspect.signature(str.replace)}")

# === Docstring extraction ===
print("\n=== Docstring extraction ===")
doc = inspect.getdoc(str.split)
first_line = doc.split("\n")[0] if doc else "No docstring"
print(f"First line of str.split docstring:\n{first_line}")

# === Type chain ===
print("\n=== Type chain ===")
print(f"type(42) = {type(42)}")
print(f"type(int) = {type(int)}")
print(f"type(type) = {type(type)}")
Solution
import inspect

# === dir() exploration ===
print("=== dir() exploration ===")
public_methods = [m for m in dir(str) if not m.startswith("_")]
print(f"Public methods on str: {', '.join(public_methods)}")

# === Signature inspection ===
print("\n=== Signature inspection ===")
print(f"str.split signature: {inspect.signature(str.split)}")
print(f"str.replace signature: {inspect.signature(str.replace)}")

# === Docstring extraction ===
print("\n=== Docstring extraction ===")
doc = inspect.getdoc(str.split)
first_line = doc.split("\n")[0] if doc else "No docstring"
print(f"First line of str.split docstring:\n{first_line}")

# === Type chain ===
print("\n=== Type chain ===")
print(f"type(42) = {type(42)}")
print(f"type(int) = {type(int)}")
print(f"type(type) = {type(type)}")

The REPL introspection toolkit:

ToolWhat it doesWhen to use it
dir(obj)Lists all attributes"What can this object do?"
type(obj)Returns the type"What kind of thing is this?"
help(obj)Full documentation"How exactly does this work?"
inspect.signature(fn)Parameter list"What arguments does this take?"
inspect.getdoc(obj)Clean docstring"Give me just the description"
inspect.getsource(fn)Source code"Show me the implementation"
hasattr(obj, name)Attribute check"Does this have a .foo?"
isinstance(obj, cls)Type check"Is this a subclass of X?"

The type chain: Everything in Python has a type. type(42) is int, type(int) is type, and type(type) is type — it is self-referential. This is the metaclass system at work.

Pro tip: In the REPL, you can combine these: [m for m in dir(obj) if 'find' in m.lower()] quickly finds all methods containing "find" in their name.

Expected Output
=== dir() exploration ===\nPublic methods on str: capitalize, casefold, center, count, encode, endswith, expandtabs, find, format, format_map, index, isalnum, isalpha, isascii, isdigit, isidentifier, islower, isnumeric, isprintable, isspace, istitle, isupper, join, ljust, lower, lstrip, maketrans, partition, removeprefix, removesuffix, replace, rfind, rindex, rjust, rpartition, rsplit, rstrip, split, splitlines, startswith, strip, swapcase, title, translate, upper, zfill\n\n=== Signature inspection ===\nstr.split signature: (sep=None, maxsplit=-1)\nstr.replace signature: (old, new, count=-1)\n\n=== Docstring extraction ===\nFirst line of str.split docstring:\nReturn a list of the substrings in the string, using sep as the separator string.\n\n=== Type chain ===\ntype(42) = <class 'int'>\ntype(int) = <class 'type'>\ntype(type) = <class 'type'>
Hints

Hint 1: `dir(obj)` returns a list of all attribute names. Filter out dunder methods (those starting with `__`) to see the "public" API. This is how experienced developers explore unfamiliar objects in the REPL.

Hint 2: The `inspect` module provides `inspect.signature()` to get function signatures and `inspect.getdoc()` for clean docstrings. These are more reliable than accessing `__doc__` directly.

#7Exploring sys.displayhook BehaviorMedium
REPLsys.displayhookcustomizationinternals

sys.displayhook is the function the REPL calls to display expression results. Replace it with a custom version that shows the type alongside each value, then restore the original.

Python
import sys
import builtins

# Save the original displayhook
original_hook = sys.displayhook

print("=== Default displayhook behavior ===")
print(f"Default hook type: {type(original_hook)}")

# Custom displayhook that shows type information
def custom_displayhook(value):
    if value is None:
        print(f"[NoneType] (suppressed)")
        return
    builtins._ = value
    print(f"[{type(value).__name__}] {repr(value)}")

# Install our custom hook
sys.displayhook = custom_displayhook

print("\n=== Custom displayhook ===")
# Simulate REPL expression evaluation by calling displayhook directly
sys.displayhook(42)
sys.displayhook("hello")
sys.displayhook([1, 2, 3])
sys.displayhook(None)

print(f"\n=== Verify _ tracking ===")
print(f"builtins._ = {builtins._}")

# Restore original
sys.displayhook = original_hook
print("\n=== Restored default displayhook ===")
Solution
import sys
import builtins

original_hook = sys.displayhook

print("=== Default displayhook behavior ===")
print(f"Default hook type: {type(original_hook)}")

def custom_displayhook(value):
if value is None:
print(f"[NoneType] (suppressed)")
return
builtins._ = value
print(f"[{type(value).__name__}] {repr(value)}")

sys.displayhook = custom_displayhook

print("\n=== Custom displayhook ===")
sys.displayhook(42)
sys.displayhook("hello")
sys.displayhook([1, 2, 3])
sys.displayhook(None)

print(f"\n=== Verify _ tracking ===")
print(f"builtins._ = {builtins._}")

sys.displayhook = original_hook
print("\n=== Restored default displayhook ===")

How sys.displayhook works internally:

The default sys.displayhook implementation (in CPython) does roughly this:

def default_displayhook(value):
if value is None:
return # Suppress None
builtins._ = value # Store in _
text = repr(value) # Get repr
sys.stdout.write(text + '\n') # Print it

Why this matters:

  • IPython replaces sys.displayhook with a rich version that supports syntax highlighting, pretty-printing of data structures, and output caching (Out[1], Out[2], etc.).
  • Jupyter notebooks use a custom displayhook that routes output to the notebook frontend instead of stdout.
  • You can use this to build logging REPLs, teaching tools, or debuggers that annotate every expression with extra metadata.

Important: Always save and restore the original hook. Permanently replacing it can break libraries that depend on the default behavior.

import sys
import builtins

def custom_displayhook(value):
    """A custom displayhook that adds type information."""
    # TODO: Show value with its type, update builtins._
    pass
Expected Output
=== Default displayhook behavior ===\nDefault hook type: <class 'builtin_function_or_method'>\n\n=== Custom displayhook ===\n[int] 42\n[str] 'hello'\n[list] [1, 2, 3]\n[NoneType] (suppressed)\n\n=== Verify _ tracking ===\nbuiltins._ = [1, 2, 3]\n\n=== Restored default displayhook ===
Hints

Hint 1: `sys.displayhook` is a callable that the REPL invokes with the result of each expression. The default implementation prints `repr(value)` and stores it in `builtins._`. You can replace it with any function that takes one argument.

Hint 2: Your custom displayhook should handle `None` specially (suppress it, like the default does). For all other values, display them however you like, then store the value in `builtins._` to maintain the `_` variable behavior.


Hard

#8Build a Mini REPL with Expression HistoryHard
REPLhistorycode-modulecustom-REPL

Build a mini REPL that tracks every expression and its result, providing IPython-style In[n]/Out[n] history. This is the core of how IPython and Jupyter track execution history.

Python
import sys
import builtins
import code
import io

class HistoryREPL:
    """A custom REPL that tracks all expressions and their results."""

    def __init__(self):
        self.history = []           # (line_number, input, output_or_None)
        self.namespace = {}
        self.console = code.InteractiveConsole(locals=self.namespace)
        self.line_count = 0
        self._current_line = ""
        self._last_result = None
        self._got_result = False

        # Install custom displayhook
        self._original_hook = sys.displayhook
        sys.displayhook = self._capture_hook

    def _capture_hook(self, value):
        """Custom displayhook that captures expression results."""
        if value is not None:
            builtins._ = value
            self._last_result = value
            self._got_result = True
            print(f"    => {repr(value)}")

    def execute(self, line):
        """Execute a line and track it in history."""
        self.line_count += 1
        self._current_line = line
        self._last_result = None
        self._got_result = False

        print(f"[{self.line_count}] >>> {line}")
        self.console.push(line)

        result = self._last_result if self._got_result else None
        self.history.append((self.line_count, line, result))

    def show_history(self):
        """Display only expressions that produced output."""
        print("\n=== Expression History ===")
        for num, inp, out in self.history:
            if out is not None:
                print(f"In[{num}]: {inp} => {repr(out)}")

    def get_output(self, n):
        """Retrieve a past result by line number."""
        for num, inp, out in self.history:
            if num == n:
                return out
        return None

    def cleanup(self):
        """Restore original displayhook."""
        sys.displayhook = self._original_hook


# Demo the HistoryREPL
repl = HistoryREPL()

repl.execute("x = 10")
repl.execute("x * 5")
repl.execute("name = 'Python'")
repl.execute("len(name)")
repl.execute("x + len(name)")
repl.execute("[i**2 for i in range(5)]")

repl.show_history()

print("\n=== Retrieve past result ===")
print(f"Out[2] = {repl.get_output(2)}")
print(f"Out[6] = {repl.get_output(6)}")

repl.cleanup()
Solution
import sys
import builtins
import code
import io

class HistoryREPL:
def __init__(self):
self.history = []
self.namespace = {}
self.console = code.InteractiveConsole(locals=self.namespace)
self.line_count = 0
self._current_line = ""
self._last_result = None
self._got_result = False
self._original_hook = sys.displayhook
sys.displayhook = self._capture_hook

def _capture_hook(self, value):
if value is not None:
builtins._ = value
self._last_result = value
self._got_result = True
print(f" => {repr(value)}")

def execute(self, line):
self.line_count += 1
self._current_line = line
self._last_result = None
self._got_result = False
print(f"[{self.line_count}] >>> {line}")
self.console.push(line)
result = self._last_result if self._got_result else None
self.history.append((self.line_count, line, result))

def show_history(self):
print("\n=== Expression History ===")
for num, inp, out in self.history:
if out is not None:
print(f"In[{num}]: {inp} => {repr(out)}")

def get_output(self, n):
for num, inp, out in self.history:
if num == n:
return out
return None

def cleanup(self):
sys.displayhook = self._original_hook

repl = HistoryREPL()
repl.execute("x = 10")
repl.execute("x * 5")
repl.execute("name = 'Python'")
repl.execute("len(name)")
repl.execute("x + len(name)")
repl.execute("[i**2 for i in range(5)]")
repl.show_history()
print("\n=== Retrieve past result ===")
print(f"Out[2] = {repl.get_output(2)}")
print(f"Out[6] = {repl.get_output(6)}")
repl.cleanup()

Architecture of a history-tracking REPL:

User Input


┌─────────────────────┐
│ HistoryREPL.execute │
│ - increment counter │
│ - record input │
└──────────┬──────────┘


┌─────────────────────────┐
│ InteractiveConsole.push │
│ - compile(mode='single')│
│ - exec() the bytecode │
└──────────┬──────────────┘
│ (if expression result)

┌──────────────────────────┐
│ custom sys.displayhook │
│ - capture result │
│ - store in builtins._ │
│ - print with annotation │
└──────────────────────────┘


┌──────────────────────┐
│ history.append(...) │
│ (num, input, output) │
└──────────────────────┘

This is conceptually identical to how IPython works internally. IPython's InteractiveShell maintains In (a list of input strings) and Out (a dict of output values), with custom displayhooks that populate both.

import code
import sys

class HistoryREPL:
    """A custom REPL that tracks all expressions and their results."""
    def __init__(self):
        self.history = []      # List of (input, output) tuples
        self.namespace = {}
        # TODO: Set up InteractiveConsole and custom displayhook

    def execute(self, line):
        # TODO: Execute line, capture output, store in history
        pass

    def show_history(self):
        # TODO: Display numbered history like IPython's Out[]
        pass
Expected Output
[1] >>> x = 10\n[2] >>> x * 5\n    => 50\n[3] >>> name = 'Python'\n[4] >>> len(name)\n    => 6\n[5] >>> x + len(name)\n    => 16\n[6] >>> [i**2 for i in range(5)]\n    => [0, 1, 4, 9, 16]\n\n=== Expression History ===\nIn[2]: x * 5 => 50\nIn[4]: len(name) => 6\nIn[5]: x + len(name) => 16\nIn[6]: [i**2 for i in range(5)] => [0, 1, 4, 9, 16]\n\n=== Retrieve past result ===\nOut[2] = 50\nOut[6] = [0, 1, 4, 9, 16]
Hints

Hint 1: Replace `sys.displayhook` with a custom function that captures expression results into a list before displaying them. Each time the hook is called with a non-None value, record both the input line and the result.

Hint 2: Use `code.InteractiveConsole` with `push()` to execute lines. Track the current input line in an instance variable so your custom displayhook knows which input produced each output.

#9Auto-Inspect Command — Type, ID, and ValueHard
REPLintrospectioninspect-modulecustom-command

Build an auto_inspect() function that takes any Python expression as a string, evaluates it, and prints a comprehensive inspection report. This is the kind of tool you would add to a custom REPL to deeply understand any object.

Python
import sys
import inspect

def auto_inspect(expression_str, namespace=None):
    """Evaluate an expression and print a full inspection report."""
    if namespace is None:
        namespace = {}

    # Evaluate the expression
    value = eval(expression_str, namespace)

    print(f"=== Inspecting: {expression_str} ===")
    print(f"  Value : {value}")
    print(f"  Repr  : {repr(value)}")
    print(f"  Type  : {type(value).__name__}")

    # Method Resolution Order
    mro = " -> ".join(cls.__name__ for cls in type(value).__mro__)
    print(f"  MRO   : {mro}")

    # Memory address
    print(f"  ID    : {id(value)}")

    # Size in bytes
    try:
        size = sys.getsizeof(value)
        print(f"  Size  : {size} bytes")
    except TypeError:
        print(f"  Size  : (not applicable)")

    # Callable check
    if callable(value):
        print(f"  Callable: Yes")
        try:
            sig = inspect.signature(value)
            print(f"  Signature: {sig}")
        except (ValueError, TypeError):
            print(f"  Signature: (not introspectable)")
    else:
        print(f"  Callable: No")

    # Length for sized objects
    if hasattr(value, '__len__') and not isinstance(value, (str, bytes)):
        print(f"  Length: {len(value)}")

    return value


# Demo inspections
auto_inspect("42")
print()
auto_inspect("'hello'")
print()
auto_inspect("len")
print()
auto_inspect("[1, 2, 3]")
Solution
import sys
import inspect

def auto_inspect(expression_str, namespace=None):
if namespace is None:
namespace = {}

value = eval(expression_str, namespace)

print(f"=== Inspecting: {expression_str} ===")
print(f" Value : {value}")
print(f" Repr : {repr(value)}")
print(f" Type : {type(value).__name__}")

mro = " -> ".join(cls.__name__ for cls in type(value).__mro__)
print(f" MRO : {mro}")

print(f" ID : {id(value)}")

try:
size = sys.getsizeof(value)
print(f" Size : {size} bytes")
except TypeError:
print(f" Size : (not applicable)")

if callable(value):
print(f" Callable: Yes")
try:
sig = inspect.signature(value)
print(f" Signature: {sig}")
except (ValueError, TypeError):
print(f" Signature: (not introspectable)")
else:
print(f" Callable: No")

if hasattr(value, '__len__') and not isinstance(value, (str, bytes)):
print(f" Length: {len(value)}")

return value

auto_inspect("42")
print()
auto_inspect("'hello'")
print()
auto_inspect("len")
print()
auto_inspect("[1, 2, 3]")

What each inspection reveals:

FieldFunctionWhy it matters
repr()Machine-readable stringShows quotes on strings, full structure of containers
type()Runtime typePython is dynamically typed — this is how you verify
MROtype().__mro__Method resolution order — shows inheritance chain
id()Memory addressUnique identifier; reveals object reuse (interning)
sys.getsizeof()Shallow byte sizeMemory profiling; note it is shallow (does not count referenced objects)
callable()Can it be called?Distinguishes functions, classes, objects with __call__
inspect.signature()Parameter specShows required/optional args, defaults, annotations

Extending this for a real REPL:

You could register this as a magic command (like IPython's %inspect) or hook it into a custom displayhook that automatically shows type information. Some developers create a .pythonrc file that loads helper functions like this into every REPL session:

# ~/.pythonrc.py
import sys, inspect
def xi(expr): ... # auto_inspect shorthand

Set PYTHONSTARTUP=~/.pythonrc.py and xi() is available in every REPL session.

import sys
import inspect

def auto_inspect(expression_str, namespace):
    """Evaluate an expression and print a full inspection report:
    - value and repr
    - type and MRO
    - id (memory address)
    - size in bytes
    - callable check + signature if applicable
    """
    # TODO: Implement the inspector
    pass
Expected Output
=== Inspecting: 42 ===\n  Value : 42\n  Repr  : 42\n  Type  : int\n  MRO   : int -> object\n  ID    : (memory address)\n  Size  : 28 bytes\n  Callable: No\n\n=== Inspecting: 'hello' ===\n  Value : hello\n  Repr  : 'hello'\n  Type  : str\n  MRO   : str -> object\n  ID    : (memory address)\n  Size  : 54 bytes\n  Callable: No\n\n=== Inspecting: len ===\n  Value : <built-in function len>\n  Repr  : <built-in function len>\n  Type  : builtin_function_or_method\n  MRO   : builtin_function_or_method -> object\n  ID    : (memory address)\n  Size  : (not applicable)\n  Callable: Yes\n  Signature: (obj, /)\n\n=== Inspecting: [1, 2, 3] ===\n  Value : [1, 2, 3]\n  Repr  : [1, 2, 3]\n  Type  : list\n  MRO   : list -> object\n  ID    : (memory address)\n  Size  : 88 bytes\n  Callable: No\n  Length: 3
Hints

Hint 1: Use `eval()` to evaluate the expression string in the given namespace. Then use `type()`, `id()`, `sys.getsizeof()`, `callable()`, and `inspect.signature()` to gather all metadata about the resulting object.

Hint 2: Wrap `sys.getsizeof()` and `inspect.signature()` in try/except blocks — some built-in types raise TypeError for getsizeof, and some callables have no introspectable signature.

#10REPL Session Replayer from Logged CommandsHard
REPLsession-replayloggingcode-module

Build a REPL session recorder that logs every command and its output, then a replayer that re-executes the session and verifies that all outputs match. This pattern is used in automated testing, documentation generation, and reproducibility verification.

Python
import sys
import builtins
import code
import json
from datetime import datetime

class REPLRecorder:
    """Records REPL commands and their outputs."""

    def __init__(self):
        self.log = []
        self.namespace = {}
        self.console = code.InteractiveConsole(locals=self.namespace)
        self._captured_output = None
        self._original_hook = sys.displayhook

    def _recording_hook(self, value):
        """Capture expression results during recording."""
        if value is not None:
            builtins._ = value
            self._captured_output = repr(value)
            print(value)

    def execute(self, line):
        """Execute a line and record it with its output."""
        self._captured_output = None
        sys.displayhook = self._recording_hook

        print(f">>> {line}")
        self.console.push(line)

        sys.displayhook = self._original_hook

        self.log.append({
            "input": line,
            "output": self._captured_output,
            "timestamp": datetime.now().isoformat(),
        })

    def save_session(self):
        """Return the session log."""
        return {
            "recorded_at": datetime.now().isoformat(),
            "commands": self.log,
        }


def replay_session(session_data):
    """Replay a recorded session and verify outputs match."""
    commands = session_data["commands"]
    namespace = {}
    console = code.InteractiveConsole(locals=namespace)
    original_hook = sys.displayhook

    total = len(commands)
    checked = 0
    passed = 0
    failed = 0

    captured = {"value": None}

    def replay_hook(value):
        if value is not None:
            builtins._ = value
            captured["value"] = repr(value)

    for i, entry in enumerate(commands):
        line = entry["input"]
        expected = entry["output"]

        print(f"[REPLAY {i+1}/{total}] {line}")

        captured["value"] = None
        sys.displayhook = replay_hook
        console.push(line)
        sys.displayhook = original_hook

        actual = captured["value"]

        if expected is None:
            print(f"  (no output expected)")
        else:
            checked += 1
            print(f"  Expected: {expected}")
            print(f"  Got     : {actual}")
            if actual == expected:
                passed += 1
                print(f"  Status  : MATCH")
            else:
                failed += 1
                print(f"  Status  : MISMATCH")

    print(f"\n=== REPLAY SUMMARY ===")
    print(f"Total: {total} | Checked: {checked} | Passed: {passed} | Failed: {failed}")


# === Demo ===
print("=== RECORDING SESSION ===")
recorder = REPLRecorder()
recorder.execute("a = 5")
recorder.execute("b = 10")
recorder.execute("a + b")
recorder.execute("result = a * b")
recorder.execute("f'The answer is {result}'")
recorder.execute("[x**2 for x in range(1, 6)]")

session = recorder.save_session()
print(f"\nSession saved: {len(session['commands'])} commands recorded")

print(f"\n=== REPLAYING SESSION ===")
replay_session(session)
Solution
import sys
import builtins
import code
import json
from datetime import datetime

class REPLRecorder:
def __init__(self):
self.log = []
self.namespace = {}
self.console = code.InteractiveConsole(locals=self.namespace)
self._captured_output = None
self._original_hook = sys.displayhook

def _recording_hook(self, value):
if value is not None:
builtins._ = value
self._captured_output = repr(value)
print(value)

def execute(self, line):
self._captured_output = None
sys.displayhook = self._recording_hook
print(f">>> {line}")
self.console.push(line)
sys.displayhook = self._original_hook
self.log.append({
"input": line,
"output": self._captured_output,
"timestamp": datetime.now().isoformat(),
})

def save_session(self):
return {
"recorded_at": datetime.now().isoformat(),
"commands": self.log,
}


def replay_session(session_data):
commands = session_data["commands"]
namespace = {}
console = code.InteractiveConsole(locals=namespace)
original_hook = sys.displayhook

total = len(commands)
checked = 0
passed = 0
failed = 0
captured = {"value": None}

def replay_hook(value):
if value is not None:
builtins._ = value
captured["value"] = repr(value)

for i, entry in enumerate(commands):
line = entry["input"]
expected = entry["output"]
print(f"[REPLAY {i+1}/{total}] {line}")

captured["value"] = None
sys.displayhook = replay_hook
console.push(line)
sys.displayhook = original_hook

actual = captured["value"]

if expected is None:
print(f" (no output expected)")
else:
checked += 1
print(f" Expected: {expected}")
print(f" Got : {actual}")
if actual == expected:
passed += 1
print(f" Status : MATCH")
else:
failed += 1
print(f" Status : MISMATCH")

print(f"\n=== REPLAY SUMMARY ===")
print(f"Total: {total} | Checked: {checked} | Passed: {passed} | Failed: {failed}")


print("=== RECORDING SESSION ===")
recorder = REPLRecorder()
recorder.execute("a = 5")
recorder.execute("b = 10")
recorder.execute("a + b")
recorder.execute("result = a * b")
recorder.execute("f'The answer is {result}'")
recorder.execute("[x**2 for x in range(1, 6)]")
session = recorder.save_session()
print(f"\nSession saved: {len(session['commands'])} commands recorded")
print(f"\n=== REPLAYING SESSION ===")
replay_session(session)

Architecture of the record/replay system:

RECORDING PHASE REPLAY PHASE
=============== =============

User input Saved session log
│ │
▼ ▼
┌────────────┐ ┌──────────────┐
│ execute() │ │ replay_session│
│ push line │ │ push line │
└──────┬─────┘ └──────┬───────┘
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ recording_hook │ │ replay_hook │
│ capture repr() │ │ capture repr() │
└──────┬─────────┘ └──────┬─────────┘
│ │
▼ ▼
┌────────────┐ ┌──────────────────┐
│ session log │ ─── save ───▶ │ compare expected │
│ [{input, │ (JSON) │ vs actual repr() │
│ output}] │ └──────────────────┘
└────────────┘

Real-world applications of this pattern:

  1. Doctest: Python's doctest module uses exactly this approach — it records expected REPL output in docstrings and replays it to verify correctness.

  2. Jupyter notebooks: Notebooks store both input cells and their outputs. The "Restart and Run All" feature is essentially a replay that regenerates all outputs.

  3. Regression testing: Record a session that exercises your library, save it, and replay after code changes to detect regressions.

  4. Teaching tools: Record an expert's REPL session and let students replay it step-by-step with explanations.

Limitations to be aware of:

  • Non-deterministic operations (random, datetime.now(), id()) will produce mismatches on replay
  • Side effects (file I/O, network calls) are not captured
  • Multi-line blocks (functions, loops) require tracking the push() return value for continuation lines
import code
import sys
import json
from datetime import datetime

class REPLRecorder:
    """Records REPL commands and outputs for later replay."""
    def __init__(self):
        # TODO: Set up recording infrastructure
        pass

    def execute(self, line):
        # TODO: Execute and record
        pass

    def save_session(self):
        # TODO: Return session log as serializable data
        pass

def replay_session(session_log):
    """Replay a recorded session, showing inputs and expected vs actual outputs."""
    # TODO: Re-execute each command, compare outputs
    pass
Expected Output
=== RECORDING SESSION ===\n>>> a = 5\n>>> b = 10\n>>> a + b\n15\n>>> result = a * b\n>>> f'The answer is {result}'\n'The answer is 50'\n>>> [x**2 for x in range(1, 6)]\n[1, 4, 9, 16, 25]\n\nSession saved: 6 commands recorded\n\n=== REPLAYING SESSION ===\n[REPLAY 1/6] a = 5\n  (no output expected)\n[REPLAY 2/6] b = 10\n  (no output expected)\n[REPLAY 3/6] a + b\n  Expected: 15\n  Got     : 15\n  Status  : MATCH\n[REPLAY 4/6] result = a * b\n  (no output expected)\n[REPLAY 5/6] f'The answer is {result}'\n  Expected: 'The answer is 50'\n  Got     : 'The answer is 50'\n  Status  : MATCH\n[REPLAY 6/6] [x**2 for x in range(1, 6)]\n  Expected: [1, 4, 9, 16, 25]\n  Got     : [1, 4, 9, 16, 25]\n  Status  : MATCH\n\n=== REPLAY SUMMARY ===\nTotal: 6 | Checked: 3 | Passed: 3 | Failed: 0
Hints

Hint 1: For recording: use a custom `sys.displayhook` to capture expression outputs as their `repr()` strings. Store each command alongside its captured output (or None for statements) in a list.

Hint 2: For replaying: iterate through the recorded commands, execute each in a fresh namespace using `compile(mode="single")` + `exec()`, capture the output with another custom displayhook, and compare the `repr()` of the replayed result against the recorded output.

© 2026 EngineersOfAI. All rights reserved.