| name | scheme-in-python-2026-05-03 |
| description | Build a Scheme interpreter in Python covering tokenization, eval/apply loop, environment frames with lexical and dynamic scoping, special forms (define/lambda/if/let/cond), Pair-based linked lists, built-in procedures, and a REPL. Use when implementing a Scheme dialect in Python, teaching programming language concepts via SICP-style interpreters, or understanding how Lisp evaluates code as data. |
Scheme Interpreter in Python
Overview
Scheme is a minimalist Lisp dialect founded on lambda calculus, lexical scoping, and homoiconicity (code is data). Building a Scheme interpreter in Python teaches the core mechanics of how programming languages work: tokenizing source text, parsing into nested structures, recursively evaluating expressions through an eval/apply loop, managing environment frames for variable binding, implementing special forms that control evaluation order, and wiring it all together in a read-eval-print loop.
This skill follows the SICP (Structure and Interpretation of Computer Programs) tradition where interpreters are built incrementally in pure Python with no external dependencies. The reference implementations from Berkeley CS61A and educational repositories provide the structural blueprint.
When to Use
- Building a Scheme interpreter from scratch in Python for learning or coursework
- Understanding how programming language evaluation works (eval/apply, environments, closures)
- Implementing lexical vs dynamic scoping in an interpreter
- Adding special forms or built-in procedures to an existing interpreter
- Debugging interpreter behavior (scoping bugs, evaluation order, macro expansion)
- Teaching programming language concepts through hands-on implementation
Core Concepts
S-Expressions and Homoiconicity
Scheme source code consists of s-expressions โ parenthesized prefix notation where the first element is the operator and remaining elements are operands:
(+ 1 2) ; arithmetic
(define x 10) ; variable binding
(lambda (x y) (+ x y)) ; anonymous procedure
Because Scheme uses lists as both code and data structures, programs can manipulate their own source. This homoiconicity means the parser produces the same data structure used at runtime.
The Eval/Apply Loop
Every interpreter reduces to two mutually recursive functions:
-
eval(expr, env) โ examines an expression and determines what to do:
- Numbers/booleans return themselves (self-evaluating)
- Symbols look up the value in the environment
- Quoted expressions return their structure unevaluated
- Special forms dispatch to custom handlers (if, define, lambda, etc.)
- Everything else is a combination: eval the operator and operands, then apply
-
apply(proc, args, env) โ invokes a procedure with evaluated arguments:
- Built-in procedures: call the underlying Python function
- User-defined procedures: bind parameters to arguments in a new frame, eval the body
Environments and Frames
An environment is a chain of frames, where each frame maps symbols to values and points to a parent frame. Variable lookup walks up the chain from the innermost frame outward:
class Frame:
def __init__(self, name, parent=None):
self.name = name
self.parent = parent
self.bindings = {}
Lexical scoping (lambda procedures) captures the defining environment at creation time. Dynamic scoping (mu procedures) resolves free variables in the calling environment at invocation time.
Special Forms vs Procedures
Procedures evaluate all arguments before application. Special forms control evaluation order โ some sub-expressions may never be evaluated:
| Form | Evaluates operands? |
|---|
if | Only the chosen branch |
define | Only the value expression |
lambda | Never (returns a procedure object) |
and/or | Left-to-right, short-circuits |
let | Only init expressions, not body until after binding |
Usage Examples
Minimal Working Interpreter (~80 lines)
This self-contained example handles arithmetic and variable definitions:
def tokenize(text):
"""Split Scheme source into tokens."""
return text.replace('(', ' ( ').replace(')', ' ) ').split()
def read(tokens):
"""Parse tokens into nested Python lists (s-expressions)."""
if not tokens:
raise SyntaxError("Unexpected end of input")
token = tokens.pop(0)
if token == '(':
exprs = []
while tokens and tokens[0] != ')':
exprs.append(read(tokens))
if not tokens:
raise SyntaxError("Unmatched parenthesis")
tokens.pop(0)
return exprs
elif token == ')':
raise SyntaxError("Unexpected ')'")
else:
try:
return int(token)
except ValueError:
try:
return float(token)
except ValueError:
return token
:
():
.name = name
.parent = parent
.bindings = {}
():
frame = Frame()
frame.bindings[] = *args: (args)
frame.bindings[] = a, b=: a - b b -a
frame.bindings[] = *args: (.join((, args)))
frame.bindings[] = a, b: a / b
frame
():
(expr, (, )):
expr
(expr, ) expr expr[] == :
expr[]
(expr, ):
lookup(env, expr)
(expr, ):
op = eval_scheme(expr[], env)
args = [eval_scheme(arg, env) arg expr[:]]
apply_scheme(op, args, env)
TypeError()
():
name env.bindings:
env.bindings[name]
env.parent :
lookup(env.parent, name)
NameError()
():
(proc):
proc(*args)
TypeError()
():
(expr[], ):
name = expr[]
value = eval_scheme(expr[], env)
env.bindings[name] = value
name
(expr[], ):
params = expr[][:]
body = expr[]
proc = make_lambda(params, body, env)
env.bindings[expr[][]] = proc
expr[][]
():
():
new_frame = Frame(, env)
param, arg (params, args):
new_frame.bindings[param] = arg
eval_scheme(body, new_frame)
proc
global_env = make_global_frame()
(, end=)
:
:
line = ()
tokens = tokenize(line)
expr = read(tokens)
(expr, ) expr[] == :
result = eval_define(expr, global_env)
:
result = eval_scheme(expr, global_env)
(result)
(EOFError, KeyboardInterrupt):
Exception e:
()
(, end=)
Run it: python3 scheme_minimal.py
scm> (+ 1 2)
3
scm> (define x 10)
x
scm> (* x 3)
30
Advanced Topics
Tokenizer and Reader: Tokenization rules, reader implementation, Pair-based linked lists, handling quoted expressions and dotted pairs โ Tokenizer and Reader
Eval/Apply Loop: Recursive evaluation architecture, self-evaluating expressions, procedure application dispatch, tail-call optimization patterns โ Eval/Apply Loop
Environments and Scoping: Frame chains, lexical vs dynamic scoping, closure creation, MuProc for SICP-style dynamic scope โ Environments and Scoping
Special Forms: define, lambda/mu, if/cond/case, let/let*/letrec, quote/quasiquote, begin/and/or, set! โ Special Forms
Builtins and Pair: Cons-cell linked lists, arithmetic operations, predicates, higher-order functions (map/filter/fold), I/O procedures, registration patterns โ Builtins and Pair
REPL and Error Handling: Multi-line input, pretty-printing Scheme values, error classification and reporting, session management โ REPL and Error Handling