Understanding the Python REPL - The Execution Engine You Already Have
Reading time: ~14 minutes | Level: Foundation → Engineering
Quick: what does this REPL session print and why?
>>> x = 10
>>> x = x + 1
>>> x
>>> print(x)
Line 3 (x) prints 11. Line 4 (print(x)) also prints 11.
But they work differently. Line 3 is the REPL printing the expression result. Line 4 is print() writing to stdout. Two different mechanisms. Same visual outcome.
If that distinction is blurry, you do not fully understand the REPL - and you are missing one of Python's most powerful debugging and exploration tools.
What You Will Learn
- What REPL stands for and what each phase actually does
- How Python compiles and executes code even in interactive mode
- Why state persists between REPL lines - and what that means for debugging
- The
_underscore variable: Python's automatic result storage - Multi-line input, indentation, and how REPL handles incomplete code
- How expressions and statements differ in REPL output
- IPython: the professional REPL that changes your workflow
- Real engineering use cases where REPL beats writing scripts
Prerequisites
- Python installed (see previous lesson)
- A terminal you can type commands into
- No prior REPL experience required
The Mental Model: A Loop Inside the Interpreter
Four phases. Repeat until the user exits.
The critical insight: Eval is not interpretation. Even in interactive mode, Python goes through the full compilation pipeline - tokenization, AST, bytecode - before executing anything. The difference from running a script file is only timing: REPL compiles and executes one statement at a time, immediately.
Watch: Python REPL and Interactive Mode
:::info Video Covers the REPL in depth, including how expressions vs statements work, state persistence, and common interactive usage patterns. :::
Part 1 - Starting the REPL and Reading Input
python3
You see:
Python 3.11.9 (main, Apr 2 2024, 08:25:00)
[GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
The >>> prompt is the REPL waiting for input. Three right-angle brackets. The Python logo.
The Read phase reads everything you type until it detects a complete syntactic unit. For a single expression like 2 + 3, one line is enough. For a function definition, it needs multiple lines.
Part 2 - The Eval Phase: Full Compilation Happens Here
This is the phase most people misunderstand.
When you press Enter, Python does not "interpret" your code in some simplified way. It runs the full pipeline:
>>> 2 + 3
What happens:
- Tokenize:
2,+,3→ three tokens - Parse: tokens →
BinOp(Constant(2), Add(), Constant(3))AST node - Compile: AST → bytecode instructions
- Execute: PVM evaluates the bytecode, produces
5
You can see the bytecode yourself:
>>> import dis
>>> dis.dis(compile("2 + 3", "<stdin>", "eval"))
1 0 LOAD_CONST 0 (2)
2 LOAD_CONST 1 (3)
4 BINARY_OP 0 (__)
6 RETURN_VALUE
Four bytecode instructions for 2 + 3. REPL does not skip the compiler - it is the compiler, executing incrementally.
Understanding that REPL compiles code before executing it explains why syntax errors appear instantly (parse phase) but logic errors appear only when that line executes.
Part 3 - The Print Phase: Expressions vs Statements
This is the subtle distinction that trips up beginners.
Expressions produce a value and the REPL displays it:
>>> 2 + 3
5
>>> "hello".upper()
'HELLO'
>>> [1, 2, 3]
[1, 2, 3]
>>> True and False
False
Statements perform actions and return None. The REPL does not display None:
>>> x = 10
>>> ← no output! Assignment returns None
>>> def greet():
... return "Hi"
...
>>> ← no output! def is a statement
>>> import sys
>>> ← no output! import is a statement
The rule: REPL prints the result of expressions. It suppresses None.
Test this:
>>> None
>>> ← suppressed (it's None)
>>> print("hello") ← this IS an expression that prints as a side effect
hello
>>> ← but print() returns None, so no REPL output after
print("hello") calls print (a function), which writes to stdout as a side effect, then returns None. The REPL sees None and suppresses it.
Part 4 - State Persistence: The REPL Session Is Alive
Every REPL session maintains a namespace - a dictionary of names to objects. Names you define persist across all lines in the same session.
>>> x = 10
>>> y = 20
>>> z = x + y
>>> z
30
>>> def double(n):
... return n * 2
...
>>> double(z)
60
>>> numbers = [1, 2, 3]
>>> numbers.append(z)
>>> numbers
[1, 2, 3, 30]
All of this is live in memory. The session namespace is equivalent to the global namespace of a running Python module.
| Name | Bound to |
|---|---|
x | int object (10) |
y | int object (20) |
z | int object (30) |
double | function object |
numbers | list object [1,2,3,30] |
Hidden State Is a Real Danger
Because state persists, you can accidentally test code with variables from previous experiments:
>>> data = [1, 2, 3]
>>> # ... some experiments ...
>>> data = None ← you forgot you did this
>>> # ... more experiments ...
>>> len(data) ← AttributeError: 'NoneType' object has no attribute ...
Restart the REPL to get a clean state when your experiments become entangled. Ctrl+D (or exit()) to exit, then reopen.
The REPL session state is the most common source of "it works in REPL but not in my script" bugs. If your script does not define something that was in your REPL session, the script fails. Always test final code in an isolated script.
Part 5 - The Underscore Variable _
Python automatically stores the result of the last expression in the special name _:
>>> 10 + 20
30
>>> _
30
>>> "hello".upper()
'HELLO'
>>> _
'HELLO'
>>> [1, 2, 3]
[1, 2, 3]
>>> len(_) ← _ holds the list from the previous expression
3
This is useful for quick follow-up operations without assigning a name:
>>> some_complex_calculation()
1234567
>>> _ * 2 ← use the previous result
2469134
>>> result = _ ← give it a name if you need to keep it
_ is a real variable name in Python's namespace. It works in REPL sessions specifically because REPL assigns to it after each expression. In regular scripts, _ is just a conventional name for "don't care" values (e.g., in unpacking: first, _, last = (1, 2, 3)).
Part 6 - Multi-Line Input
When you type a statement that requires multiple lines, the REPL shows ... (the continuation prompt):
>>> def greet(name):
... message = f"Hello, {name}!"
... return message
...
>>> greet("Engineer")
'Hello, Engineer!'
The REPL knows the function definition is incomplete after line 1 (the colon signals a block is coming). It keeps waiting until you enter an empty line, signaling the block is done.
Same for if statements, for loops, class definitions, and with blocks:
>>> for i in range(3):
... if i % 2 == 0:
... print(f"{i} is even")
...
0 is even
2 is even
If you get stuck in ... mode and want to cancel, press Ctrl+C. This raises KeyboardInterrupt and returns you to >>>.
Part 7 - REPL vs Script: When to Use Which
| REPL | Script File |
|---|---|
| Quick experiments | Production code |
| Exploring library APIs | Reproducible execution |
| Debugging isolated logic | Importing as module |
| Checking object behavior | Automated runs |
| One-off calculations | Team-shared code |
| Learning new syntax | CI/CD pipelines |
The REPL is not a replacement for writing code in files. It is a verification and exploration tool. Engineers use both constantly.
Watch: Python Variables and Interactive Exploration
Part 8 - IPython: The Professional REPL
The standard Python REPL is functional but limited. IPython is what professional engineers actually use for interactive work.
Installing IPython
pip install ipython
ipython
What IPython Adds
Tab completion:
In [1]: import numpy as np
In [2]: np.<TAB>
# Shows: np.array, np.zeros, np.ones, np.linspace, ...
Magic commands:
In [1]: %timeit [x**2 for x in range(1000)]
# 156 µs ± 2.3 µs per loop
In [2]: %who
# Lists all variables in namespace
In [3]: %run my_script.py
# Run a script inside IPython, keeping its namespace
In [4]: %%time
...: result = sum(range(1_000_000))
# Shows wall time for the entire cell
In [5]: %paste
# Pastes clipboard content and executes it
Question mark for documentation:
In [1]: str.split?
# Shows docstring and signature
In [2]: str.split??
# Shows source code if available
History and output numbering:
In [1]: 2 + 2
Out[1]: 4
In [2]: "hello"
Out[2]: 'hello'
In [3]: Out[1] + Out[1] ← access previous outputs by number
Out[3]: 8
Error tracebacks: IPython shows colorized, more readable tracebacks.
Jupyter Notebooks
IPython powers Jupyter Notebooks - the standard tool for data science, ML research, and exploratory analysis. The notebook interface runs IPython kernels, giving you the same interactive capabilities with visual output of plots and formatted data.
Part 9 - Engineering Use Cases
Exploring an Unfamiliar Library
>>> import pandas as pd
>>> df = pd.DataFrame({'a': [1,2,3], 'b': [4,5,6]})
>>> type(df)
<class 'pandas.core.frame.DataFrame'>
>>> dir(df)
>>> df.dtypes
>>> df.describe()
You learn the API by touching it, not by reading documentation for 20 minutes.
Verifying Memory Behavior
>>> a = [1, 2, 3]
>>> b = a
>>> id(a) == id(b)
True
>>> b.append(4)
>>> a
[1, 2, 3, 4]
>>> id(a) == id(b)
True
REPL makes the object model visible and tangible.
Debugging a Function in Isolation
>>> def process(data):
... return sorted(set(data))
...
>>> process([3, 1, 2, 1, 3])
[1, 2, 3]
>>> process([])
[]
>>> process(["c", "a", "b", "a"])
['a', 'b', 'c']
Test the function with multiple inputs without writing a test file.
Quick Math and Calculations
>>> import math
>>> math.sqrt(2)
1.4142135623730951
>>> math.pi * 10**2 ← area of circle, radius 10
314.1592653589793
AI/ML Real-World Connection
The REPL (and specifically Jupyter notebooks, which are powered by IPython's execution model) is the primary workspace for ML engineering:
# In a Jupyter cell - same REPL semantics:
import numpy as np
import torch
# Explore tensor behavior interactively
x = torch.randn(3, 3)
x
# Check dtype, device, shape
x.dtype, x.device, x.shape
# Verify gradient tracking
x.requires_grad_(True)
y = x @ x.T # matrix multiply
y.sum().backward()
x.grad # check gradients
Jupyter's cell-by-cell execution model is REPL with persistence and visualization. Every data scientist uses it. Understanding REPL fundamentals makes Jupyter intuitive.
Common Mistakes
Mistake 1: Testing Stale State
>>> model_weights = load_model() # loads something
>>> # 20 lines of experiments later...
>>> model_weights # has been modified during experiments
Always restart the kernel/REPL before final validation.
Mistake 2: Confusing Expression Output with print()
>>> [1, 2, 3] ← REPL shows this (expression result)
[1, 2, 3]
>>> print([1, 2, 3]) ← stdout write, print() returns None
[1, 2, 3]
Both display the same text, but the mechanism is different. In scripts, only print() displays output. The REPL's automatic display does not happen in .py files.
Mistake 3: Expecting Variables to Reset
>>> x = 10
>>> # close terminal, reopen, start new REPL session
>>> x
NameError: name 'x' is not defined
REPL state lives only as long as the session. There is no persistence between sessions.
Interview Questions
Q1: What does REPL stand for and what does each phase do?
Answer: Read-Eval-Print-Loop. Read: receives user input. Eval: compiles the input through Python's full pipeline (tokenize → parse → compile → execute via PVM). Print: displays the expression result if non-None. Loop: returns to the prompt for next input.
Q2: Does the REPL skip bytecode compilation?
Answer: No. Even in interactive mode, Python goes through the full compilation pipeline: tokenization, AST parsing, bytecode compilation, then PVM execution. The only difference from running a script is that this happens per-statement in real time instead of for the whole file upfront.
Q3: Why does assigning a variable in the REPL produce no output?
Answer: Assignment (x = 10) is a statement, not an expression. Statements do not return values (they return None implicitly). The REPL only displays non-None expression results. Since assignment returns None, nothing is displayed.
Q4: What is the _ variable in the REPL?
Answer: _ is automatically bound to the result of the last expression evaluated in the REPL session. It allows you to use the previous result without assigning it a name. For example: 100 * 200 produces 20000, and then _ + 1 gives 20001.
Q5: What is the difference between REPL and running a script?
Answer: REPL executes code interactively - one statement at a time, maintaining state, displaying expression results automatically, and waiting for more input. A script (python file.py) executes the entire file sequentially, exits when done, destroys all state, and requires explicit print() for output. Both use the same interpreter and compilation pipeline.
Q6: Why might code that works in REPL fail in a script?
Answer: REPL accumulates state across many lines and experiments. A script starts fresh with an empty namespace. If your code depends on a variable you set up in the REPL session but forgot to include in the script, the script fails. Always test final code in a clean script execution.
Quick Reference Cheatsheet
| Action | REPL Command |
|---|---|
| Start REPL | python3 |
| Exit REPL | Ctrl+D or exit() |
| Cancel incomplete input | Ctrl+C |
| See last expression result | _ |
| Clear screen | Ctrl+L (macOS/Linux) |
| Start IPython | ipython |
| Time a statement (IPython) | %timeit statement |
| Run a script (IPython) | %run script.py |
| List namespace (IPython) | %who |
| Get help on object (IPython) | object? |
Graded Practice Challenges
Level 1 - Predict the Output
What does each line produce in the REPL?
>>> 5 * 5
>>> x = 5 * 5
>>> x
>>> print(x)
>>> _
Show Answer
25 ← expression, REPL displays
← assignment, no output (returns None)
25 ← expression (name lookup), REPL displays
25 ← print() writes to stdout as side effect
25 ← _ holds the last expression result (25 from line 4...
wait: print(x) returns None, so _ is None)
Actually: print(x) is an expression that returns None
So _ becomes None, and None is suppressed
Correction: after print(x), _ is None (print returns None). This is a subtle trap.
Level 2 - Debug the Session
A developer reports: "I tested my function in the REPL and it worked, but in my script it fails with NameError: name 'config' is not defined."
The function is:
def get_timeout():
return config["timeout"]
What is the most likely cause and fix?
Show Answer
The developer set config = {...} in the REPL session during testing. The function get_timeout() finds config in the REPL's global namespace. In the script, config was never defined - the REPL state is not present.
Fix: The script must define config before calling get_timeout(), or pass config as a parameter:
def get_timeout(config):
return config["timeout"]
Or define it at module level in the script.
Level 3 - Design Challenge
Design a workflow using the REPL for the following task: you have a CSV file and want to explore its structure, check data types, find missing values, and test a cleaning function before writing the final script.
Describe the exact sequence of REPL commands you would use.
Show Answer
>>> import pandas as pd
# Load and inspect
>>> df = pd.read_csv("data.csv")
>>> df.shape # rows, columns
>>> df.dtypes # column types
>>> df.head() # first 5 rows
>>> df.isnull().sum() # missing value counts
# Explore a specific column
>>> df['age'].describe()
>>> df['age'].unique()
>>> df['age'].value_counts()
# Test a cleaning function
>>> def clean_age(series):
... return series.fillna(series.median()).astype(int)
...
>>> cleaned = clean_age(df['age'])
>>> cleaned.isnull().sum() # should be 0
>>> cleaned.dtype # should be int64
# Test edge case
>>> clean_age(pd.Series([None, None, None])) # all missing
# Satisfied - now write to script
This workflow lets you verify each step interactively before committing to a script.
Key Takeaways
- REPL stands for Read-Eval-Print-Loop - each phase has a specific job
- Eval is full compilation - tokenize, parse, bytecode, PVM - not simplified interpretation
- Expressions display their result in REPL; statements do not
_stores the last expression result - use it for quick follow-up operations- State persists across all lines in a session - stale state is a real debugging hazard
- Restart REPL when experiments become entangled or before final validation
- IPython adds tab completion, magic commands, history, and documentation access
- Jupyter notebooks run on IPython's execution model - same REPL semantics, visual output
- REPL is an exploration and verification tool, not a replacement for writing scripts
- Real engineers use the REPL daily - it is the fastest way to understand Python behavior
