Interpreter
Defines a grammar and interpreter for a language.
Interpreter Pattern: A Complete Guide with Examples in 11 Programming Languages
The Interpreter Pattern is a behavioral design pattern that defines a grammar for a language and provides an interpreter to deal with that grammar. Given a language — whether a simple expression evaluator, a query language, a rule engine, a configuration DSL, or a scripting system — you represent each grammar rule as a class. Sentences in the language are represented as syntax trees built from these classes, and interpreting a sentence means walking the tree and calling interpret() on each node. When a problem recurs in a well-described, parseable language and performance isn't the primary concern, the Interpreter Pattern gives you an elegant, extensible way to build your own language processor from first principles.
In this comprehensive guide, we'll explore the Interpreter Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.
Table of Contents
- What is the Interpreter Pattern?
- Why Use the Interpreter Pattern?
- Interpreter Pattern Comparison
- Interpreter Pattern Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Interpreter Pattern?
The Interpreter Pattern gives you a way to evaluate sentences in a formal language by mapping each grammatical rule to a class. You build a syntax tree (Abstract Syntax Tree, or AST) from expression objects, then call interpret() on the root. The tree evaluates itself recursively — each node knows how to evaluate itself by calling interpret() on its children.
Think of it like a calculator. The expression (3 + 4) * 2 isn't evaluated in one step — it's a tree: a Multiply node whose left child is an Add node (holding 3 and 4) and whose right child is a Number node (holding 2). Calling interpret() on the Multiply node triggers calls on its children, which resolve left to right up the tree.
Why Use the Interpreter Pattern?
The Interpreter Pattern offers several compelling benefits:
- Domain-Specific Languages (DSLs): Build mini-languages tailored to your problem domain — query languages, rule engines, configuration formats, math expression parsers
- Grammar Extensibility: Adding a new operation means adding a new expression class — existing classes are untouched
- Composability: Complex expressions are composed from simpler ones using the same interface — unlimited nesting without special cases
- Separation of Concerns: Grammar rules, parsing, and evaluation are cleanly separated
- Readability: The code mirrors the grammar directly — each class IS a grammar rule
- Testability: Each expression class can be unit-tested independently with known inputs and expected outputs
- Reusable Contexts: The
Contextobject carries shared state (variable bindings, execution environment) cleanly across the entire tree traversal
Interpreter Pattern Comparison
Let's compare the Interpreter Pattern with related patterns:
| Pattern | Purpose | Structure | Extensibility | Use Case |
|---|---|---|---|---|
| Interpreter | Evaluate sentences in a grammar | AST of expression objects | Add new rules as new classes | DSLs, expression evaluators, rule engines |
| Composite | Treat individual/group objects uniformly | Tree of component objects | Add new component types | File systems, UI hierarchies |
| Visitor | Add operations to an object structure without changing it | Visitor + element hierarchy | Add new operations as visitors | AST transformations, code generation |
| Strategy | Swap algorithms at runtime | Single selected algorithm | Add new strategies | Sorting, pricing, compression |
| Command | Encapsulate a request as an object | Command + invoker + receiver | Add new command types | Undo/redo, job queues |
Key Distinctions:
- Interpreter vs. Composite: Composite is about structural hierarchy (trees of objects with uniform treatment). Interpreter uses a similar tree structure but for the specific purpose of evaluating language expressions. Every Interpreter AST is a Composite, but not every Composite is an Interpreter.
- Interpreter vs. Visitor: Both walk an AST. Interpreter bakes the evaluation logic into each node class (
interpret()is a method on the node). Visitor separates the operation from the structure — the node accepts a visitor that carries the logic. Use Visitor when you need multiple independent operations on the same AST (compile, format, evaluate, lint) without changing node classes.
Interpreter Pattern Explained
The Interpreter Pattern typically involves:
Core Components:
- AbstractExpression: Declares the
interpret(context)method that every grammar rule implements. All terminal and non-terminal expressions share this interface. - TerminalExpression: Represents the leaves of the grammar — the atoms that don't decompose further. Examples: a literal number, a variable name, a boolean constant.
- NonTerminalExpression: Represents grammar rules that combine other expressions. Contains references to child expressions and calls
interpret()on them. Examples: addition, AND, NOT, function call. - Context: Carries global information needed across the entire interpretation — variable bindings, the current scope, input stream, output buffer. Passed to every
interpret()call. - Client: Builds the AST (parses the language or constructs the tree directly) and triggers interpretation by calling
interpret(context)on the root.
Expression Tree Structure:
Expression: (x + 3) * (y - 1)
Multiply
/ \
Add Subtract
/ \ / \
Var(x) Num(3) Var(y) Num(1)
Common Interpreter Variants:
- Expression Evaluator: Evaluates mathematical or boolean expressions
- Rule Engine: Evaluates business rules (if-then conditions) against a data context
- Query Interpreter: Evaluates filter/search queries against a collection
- Configuration DSL: Interprets a custom configuration language
- Template Engine: Evaluates variable substitutions and control flow in templates
- Regular Expression Engine: Interprets regex patterns against input strings
Class Diagram
Here's the UML class diagram showing the Interpreter structure:
Beginner-Friendly Example
Let's start with the most intuitive example: a mathematical expression evaluator. We build an AST from number literals, variable lookups, and arithmetic operations, then interpret it with a variable context.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Dict
# Context — carries variable bindings
class Context:
def __init__(self) -> None:
self._vars: Dict[str, float] = {}
def set(self, name: str, value: float) -> None:
self._vars[name] = value
def get(self, name: str) -> float:
if name not in self._vars:
raise NameError(f"Undefined variable: '{name}'")
return self._vars[name]
# Abstract Expression
class Expression(ABC):
@abstractmethod
def interpret(self, ctx: Context) -> float:
pass
def __repr__(self) -> str:
return self.__class__.__name__
# ── Terminal Expressions ──────────────────────────────────────
class NumberExpression(Expression):
"""Leaf node: a literal number."""
def __init__(self, value: float) -> None:
self._value = value
def interpret(self, ctx: Context) -> float:
return self._value
def __repr__(self) -> str:
return str(self._value)
class VariableExpression(Expression):
"""Leaf node: looks up a variable in the context."""
def __init__(self, name: str) -> None:
self._name = name
def interpret(self, ctx: Context) -> float:
return ctx.get(self._name)
def __repr__(self) -> str:
return self._name
# ── Non-Terminal Expressions ──────────────────────────────────
class AddExpression(Expression):
def __init__(self, left: Expression, right: Expression) -> None:
self._left = left
self._right = right
def interpret(self, ctx: Context) -> float:
return self._left.interpret(ctx) + self._right.interpret(ctx)
def __repr__(self) -> str:
return f"({self._left} + {self._right})"
class SubtractExpression(Expression):
def __init__(self, left: Expression, right: Expression) -> None:
self._left = left
self._right = right
def interpret(self, ctx: Context) -> float:
return self._left.interpret(ctx) - self._right.interpret(ctx)
def __repr__(self) -> str:
return f"({self._left} - {self._right})"
class MultiplyExpression(Expression):
def __init__(self, left: Expression, right: Expression) -> None:
self._left = left
self._right = right
def interpret(self, ctx: Context) -> float:
return self._left.interpret(ctx) * self._right.interpret(ctx)
def __repr__(self) -> str:
return f"({self._left} * {self._right})"
class DivideExpression(Expression):
def __init__(self, left: Expression, right: Expression) -> None:
self._left = left
self._right = right
def interpret(self, ctx: Context) -> float:
divisor = self._right.interpret(ctx)
if divisor == 0:
raise ZeroDivisionError("Division by zero in expression")
return self._left.interpret(ctx) / divisor
def __repr__(self) -> str:
return f"({self._left} / {self._right})"
class NegateExpression(Expression):
"""Unary negation."""
def __init__(self, operand: Expression) -> None:
self._operand = operand
def interpret(self, ctx: Context) -> float:
return -self._operand.interpret(ctx)
def __repr__(self) -> str:
return f"(-{self._operand})"
# ── Client ────────────────────────────────────────────────────
if __name__ == "__main__":
ctx = Context()
ctx.set("x", 10.0)
ctx.set("y", 4.0)
ctx.set("z", 2.0)
# Build AST for: (x + 3) * (y - z)
# Expected: (10 + 3) * (4 - 2) = 13 * 2 = 26
expr1 = MultiplyExpression(
AddExpression(VariableExpression("x"), NumberExpression(3)),
SubtractExpression(VariableExpression("y"), VariableExpression("z"))
)
print(f"Expression : {expr1}")
print(f"Result : {expr1.interpret(ctx)}") # 26.0
# Build AST for: x / (y * z) + 1
# Expected: 10 / (4 * 2) + 1 = 10/8 + 1 = 2.25
expr2 = AddExpression(
DivideExpression(
VariableExpression("x"),
MultiplyExpression(VariableExpression("y"), VariableExpression("z"))
),
NumberExpression(1)
)
print(f"\nExpression : {expr2}")
print(f"Result : {expr2.interpret(ctx)}") # 2.25
# Re-use the same AST with different context values
ctx.set("x", 100.0)
ctx.set("y", 5.0)
ctx.set("z", 4.0)
print(f"\nSame AST, new context (x=100, y=5, z=4):")
print(f"Result : {expr1.interpret(ctx)}") # (100+3)*(5-4) = 103.0
Production-Ready Example
Now let's tackle a realistic scenario: a business rule engine with a DSL parser. The system evaluates boolean rules like age >= 18 AND (country == "US" OR country == "CA") AND NOT banned against a data context. A simple recursive-descent parser tokenizes and parses the rule string into an AST, then the AST is evaluated against each record.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
import re
# ─── Context ──────────────────────────────────────────────────────
class RuleContext:
def __init__(self, data: Dict[str, Any]) -> None:
self._data = data
def get(self, field: str) -> Any:
if field not in self._data:
raise KeyError(f"Unknown field: '{field}'")
return self._data[field]
# ─── Abstract Expression ──────────────────────────────────────────
class RuleExpression(ABC):
@abstractmethod
def interpret(self, ctx: RuleContext) -> bool:
pass
@abstractmethod
def __str__(self) -> str:
pass
# ─── Terminal Expressions ─────────────────────────────────────────
class FieldComparisonExpr(RuleExpression):
"""field OP value (e.g. age >= 18, country == "US")"""
OPERATORS = {
"==": lambda a, b: a == b,
"!=": lambda a, b: a != b,
">": lambda a, b: a > b,
">=": lambda a, b: a >= b,
"<": lambda a, b: a < b,
"<=": lambda a, b: a <= b,
}
def __init__(self, field: str, op: str, value: Any) -> None:
if op not in self.OPERATORS:
raise ValueError(f"Unknown operator: '{op}'")
self._field = field
self._op = op
self._value = value
def interpret(self, ctx: RuleContext) -> bool:
actual = ctx.get(self._field)
return self.OPERATORS[self._op](actual, self._value)
def __str__(self) -> str:
v = f'"{self._value}"' if isinstance(self._value, str) else str(self._value)
return f"{self._field} {self._op} {v}"
class BoolLiteralExpr(RuleExpression):
def __init__(self, value: bool) -> None:
self._value = value
def interpret(self, ctx: RuleContext) -> bool:
return self._value
def __str__(self) -> str:
return str(self._value)
# ─── Non-Terminal Expressions ─────────────────────────────────────
class AndExpression(RuleExpression):
def __init__(self, left: RuleExpression, right: RuleExpression) -> None:
self._left = left
self._right = right
def interpret(self, ctx: RuleContext) -> bool:
# Short-circuit: only evaluate right if left is True
return self._left.interpret(ctx) and self._right.interpret(ctx)
def __str__(self) -> str:
return f"({self._left} AND {self._right})"
class OrExpression(RuleExpression):
def __init__(self, left: RuleExpression, right: RuleExpression) -> None:
self._left = left
self._right = right
def interpret(self, ctx: RuleContext) -> bool:
return self._left.interpret(ctx) or self._right.interpret(ctx)
def __str__(self) -> str:
return f"({self._left} OR {self._right})"
class NotExpression(RuleExpression):
def __init__(self, operand: RuleExpression) -> None:
self._operand = operand
def interpret(self, ctx: RuleContext) -> bool:
return not self._operand.interpret(ctx)
def __str__(self) -> str:
return f"NOT({self._operand})"
# ─── Lexer ────────────────────────────────────────────────────────
@dataclass
class Token:
type: str
value: Any
def __repr__(self) -> str:
return f"Token({self.type}, {self.value!r})"
class Lexer:
TOKEN_PATTERNS = [
(r'\bAND\b', 'AND'),
(r'\bOR\b', 'OR'),
(r'\bNOT\b', 'NOT'),
(r'\bTRUE\b', 'TRUE'),
(r'\bFALSE\b', 'FALSE'),
(r'>=|<=|==|!=|>|<', 'OP'),
(r'"[^"]*"', 'STRING'),
(r'\d+\.\d+', 'FLOAT'),
(r'\d+', 'INT'),
(r'[a-zA-Z_][a-zA-Z0-9_]*', 'FIELD'),
(r'\(', 'LPAREN'),
(r'\)', 'RPAREN'),
(r'\s+', 'SKIP'),
]
def tokenize(self, text: str) -> List[Token]:
tokens: List[Token] = []
pos = 0
while pos < len(text):
matched = False
for pattern, ttype in self.TOKEN_PATTERNS:
m = re.match(pattern, text[pos:])
if m:
if ttype != 'SKIP':
raw = m.group(0)
if ttype == 'STRING':
value: Any = raw[1:-1] # Strip quotes
elif ttype == 'FLOAT':
value = float(raw)
elif ttype == 'INT':
value = int(raw)
elif ttype in ('TRUE', 'FALSE'):
value = ttype == 'TRUE'
else:
value = raw
tokens.append(Token(ttype, value))
pos += m.end()
matched = True
break
if not matched:
raise SyntaxError(f"Unexpected character at position {pos}: '{text[pos]}'")
tokens.append(Token('EOF', None))
return tokens
# ─── Recursive Descent Parser ─────────────────────────────────────
class Parser:
"""
Grammar:
expr → or_expr
or_expr → and_expr ('OR' and_expr)*
and_expr→ not_expr ('AND' not_expr)*
not_expr→ 'NOT' not_expr | primary
primary → '(' expr ')' | comparison | bool_literal
comparison → FIELD OP (STRING | INT | FLOAT)
"""
def __init__(self, tokens: List[Token]) -> None:
self._tokens = tokens
self._pos = 0
def _current(self) -> Token:
return self._tokens[self._pos]
def _consume(self, expected_type: Optional[str] = None) -> Token:
tok = self._current()
if expected_type and tok.type != expected_type:
raise SyntaxError(f"Expected {expected_type}, got {tok.type} ({tok.value!r})")
self._pos += 1
return tok
def parse(self) -> RuleExpression:
expr = self._parse_or()
self._consume('EOF')
return expr
def _parse_or(self) -> RuleExpression:
left = self._parse_and()
while self._current().type == 'OR':
self._consume('OR')
right = self._parse_and()
left = OrExpression(left, right)
return left
def _parse_and(self) -> RuleExpression:
left = self._parse_not()
while self._current().type == 'AND':
self._consume('AND')
right = self._parse_not()
left = AndExpression(left, right)
return left
def _parse_not(self) -> RuleExpression:
if self._current().type == 'NOT':
self._consume('NOT')
return NotExpression(self._parse_not())
return self._parse_primary()
def _parse_primary(self) -> RuleExpression:
tok = self._current()
if tok.type == 'LPAREN':
self._consume('LPAREN')
expr = self._parse_or()
self._consume('RPAREN')
return expr
if tok.type in ('TRUE', 'FALSE'):
self._consume()
return BoolLiteralExpr(tok.value)
if tok.type == 'FIELD':
field = self._consume('FIELD').value
op = self._consume('OP').value
val_tok = self._consume()
if val_tok.type not in ('STRING', 'INT', 'FLOAT'):
raise SyntaxError(f"Expected value after operator, got {val_tok.type}")
return FieldComparisonExpr(field, op, val_tok.value)
raise SyntaxError(f"Unexpected token: {tok}")
# ─── Rule Engine ──────────────────────────────────────────────────
class RuleEngine:
def __init__(self) -> None:
self._lexer = Lexer()
self._rules: Dict[str, RuleExpression] = {}
def add_rule(self, name: str, rule_text: str) -> None:
tokens = self._lexer.tokenize(rule_text)
ast = Parser(tokens).parse()
self._rules[name] = ast
print(f"[Engine] Registered rule '{name}': {ast}")
def evaluate(self, name: str, data: Dict[str, Any]) -> bool:
if name not in self._rules:
raise KeyError(f"Unknown rule: '{name}'")
ctx = RuleContext(data)
return self._rules[name].interpret(ctx)
def evaluate_all(self, data: Dict[str, Any]) -> Dict[str, bool]:
ctx = RuleContext(data)
return {name: expr.interpret(ctx) for name, expr in self._rules.items()}
# ─── Client ────────────────────────────────────────────────────────
if __name__ == "__main__":
engine = RuleEngine()
# Register business rules as DSL strings
engine.add_rule(
"eligible_for_service",
'age >= 18 AND (country == "US" OR country == "CA") AND NOT banned'
)
engine.add_rule(
"premium_user",
'subscription == "premium" AND account_age >= 365'
)
engine.add_rule(
"needs_verification",
'age < 25 AND (country == "US" OR country == "UK") AND verified == 0'
)
print()
# Test records
records = [
{"name": "Alice", "age": 30, "country": "US", "banned": False,
"subscription": "premium", "account_age": 400, "verified": 1},
{"name": "Bob", "age": 16, "country": "US", "banned": False,
"subscription": "free", "account_age": 10, "verified": 0},
{"name": "Carol", "age": 25, "country": "UK", "banned": False,
"subscription": "premium", "account_age": 200, "verified": 0},
{"name": "Dave", "age": 40, "country": "MX", "banned": False,
"subscription": "free", "account_age": 100, "verified": 1},
{"name": "Eve", "age": 22, "country": "CA", "banned": True,
"subscription": "premium", "account_age": 500, "verified": 1},
]
print("=== Rule Engine Evaluation ===\n")
header = f"{'Name':<8} {'eligible_for_service':<22} {'premium_user':<14} {'needs_verification'}"
print(header)
print("-" * len(header))
for record in records:
results = engine.evaluate_all(record)
row = (f"{record['name']:<8} "
f"{'✓' if results['eligible_for_service'] else '✗':<22} "
f"{'✓' if results['premium_user'] else '✗':<14} "
f"{'✓' if results['needs_verification'] else '✗'}")
print(row)
Real-World Use Cases
1. SQL WHERE Clause Evaluation
A SQL engine parses WHERE age > 30 AND city = 'London' into an AST of comparison and logical expressions, then evaluates that AST against each row. Each comparison is a TerminalExpression; AND/OR are NonTerminalExpressions.
2. Regular Expression Engines
A regex pattern like (a|b)*c is parsed into an AST of Concatenation, Alternation, KleeneStar, and Literal nodes. Matching the pattern against a string means walking the AST with the input string as the context.
3. Spreadsheet Formula Evaluation
Excel's =SUM(A1:A10) * IF(B1 > 100, 0.9, 1.0) is an AST of function calls, arithmetic operations, and cell references. Each recalculation walks the tree with the current cell values as the context.
4. Configuration DSLs
Tools like Terraform HCL, Nginx config, and Apache .htaccess files are domain-specific languages interpreted at load time. Each configuration directive is a TerminalExpression; blocks and conditionals are NonTerminalExpressions.
5. Business Rule Engines
BRMS systems (Drools, Easy Rules) evaluate rules like IF customer.tier == "gold" AND order.total > 500 THEN discount = 0.15. The Interpreter Pattern provides the AST evaluation backbone.
6. Search Query Languages
Elasticsearch's query DSL, Lucene query parser, and GitHub's code search all parse user search strings into expression trees (must, should, must_not → AND, OR, NOT) and evaluate them against indexed documents.
7. Template Engines
Jinja2, Mustache, and Handlebars parse template syntax ({% if user.premium %} ... {% endif %}) into ASTs of literal text, variable substitutions, and control flow nodes, then interpret them with a data context.
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Using eval() Instead of Building an Interpreter
# BAD: eval() is a security disaster — arbitrary code execution
def evaluate_rule(rule_str: str, context: dict) -> bool:
return eval(rule_str, {}, context) # NEVER DO THIS with user input!
# Someone passes: "rule_str = '__import__('os').system('rm -rf /')'"
# → Remote code execution
# GOOD: Build a proper interpreter with a parser and explicit AST
engine = RuleEngine()
engine.add_rule("check", 'age >= 18 AND country == "US"')
result = engine.evaluate("check", {"age": 25, "country": "US"})
❌ Mistake #2: Letting the Context Carry Mutable Global State Between Evaluations
# BAD: Context is shared and mutated across multiple interpret() calls
# Expressions modify the context — later expressions see side effects from earlier ones
class BadSumExpr(Expression):
def interpret(self, ctx: Context) -> float:
ctx.set("accumulator", ctx.get("accumulator") + self._value) # Mutates shared context!
return ctx.get("accumulator")
# Running two evaluations on the same context gives wrong results for the second.
# GOOD: Keep the context immutable during a single interpretation pass
# Use the return value to carry results; don't mutate variables that affect other branches
class SumExpr(Expression):
def interpret(self, ctx: Context) -> float:
return sum(e.interpret(ctx) for e in self._operands) # Pure — no side effects
❌ Mistake #3: Deeply Nested ASTs Causing Stack Overflow on Recursion
# BAD: A chain of 10,000 AND expressions hits Python's default recursion limit (1000)
exprs = [FieldComparisonExpr("x", ">", i) for i in range(10_000)]
root = exprs[0]
for e in exprs[1:]:
root = AndExpression(root, e)
root.interpret(ctx) # RecursionError: maximum recursion depth exceeded
# GOOD: Either increase the recursion limit (ugly) or implement iterative evaluation
import sys
sys.setrecursionlimit(100_000) # Emergency fix only
# Better: Flatten deep AND/OR chains into list-based MultiAndExpression
class MultiAndExpression(RuleExpression):
def __init__(self, operands: list[RuleExpression]) -> None:
self._operands = operands
def interpret(self, ctx: RuleContext) -> bool:
return all(e.interpret(ctx) for e in self._operands) # Iterative — no recursion depth issue
Frequently Asked Questions
Q1: What is the difference between the Interpreter Pattern and just using eval()?
eval() (in Python, JavaScript, PHP, etc.) executes arbitrary code in the host language — it is powerful but catastrophically insecure with any user-supplied input. The Interpreter Pattern builds a controlled, sandboxed evaluator for a specific, constrained language. Your interpreter only understands the operations you explicitly implement. A user can write age >= 18 AND country == "US" but cannot execute import os; os.system("rm -rf /"). The Interpreter Pattern is the safe, extensible, auditable alternative to eval().
Q2: When should I use the Interpreter Pattern vs. a full parser generator (ANTLR, PLY, Lark)?
Use Interpreter Pattern directly when:
- The grammar is small and stable (fewer than ~20 rules)
- You're building an internal API that constructs ASTs programmatically (not parsing text)
- The DSL is simple enough that a handwritten recursive-descent parser fits in a few hundred lines
Use a parser generator when:
- The grammar is large, complex, or will grow significantly
- You need production-grade error messages and recovery
- The language is being exposed to end users who will type it manually
- You need to support multiple languages or grammar dialects
Even with a parser generator, the Interpreter Pattern governs how the AST is evaluated once parsed.
Q3: Is the Interpreter Pattern related to the Composite Pattern?
Yes — the Interpreter Pattern is essentially a specialized application of Composite. Both use a tree of objects that implement the same interface. The difference is purpose: Composite is about treating individual items and groups uniformly (e.g., a file system). Interpreter uses the same tree structure to represent and evaluate a formal language grammar. Every Interpreter AST IS a Composite tree; Composite doesn't have to be an Interpreter.
Q4: How do I add a new operation to an existing Interpreter without modifying every expression class?
This is where the Visitor Pattern shines. If you want to add multiple independent operations (evaluate, pretty-print, optimize, type-check) to the same AST without modifying every expression class, combine Interpreter with Visitor:
class Visitor(ABC):
@abstractmethod
def visit_number(self, expr: NumberExpr) -> Any: pass
@abstractmethod
def visit_add(self, expr: AddExpr) -> Any: pass
class EvaluatorVisitor(Visitor):
def visit_number(self, expr: NumberExpr) -> float: return expr.value
def visit_add(self, expr: AddExpr) -> float:
return expr.left.accept(self) + expr.right.accept(self)
class PrettyPrinterVisitor(Visitor):
def visit_number(self, expr: NumberExpr) -> str: return str(expr.value)
def visit_add(self, expr: AddExpr) -> str:
return f"({expr.left.accept(self)} + {expr.right.accept(self)})"
Each new operation is a new Visitor class — no existing expression class is modified.
Q5: How do I handle operator precedence in a hand-written parser?
Use a recursive-descent parser where each precedence level corresponds to a parsing function. Higher precedence operators are parsed deeper in the call stack:
# Precedence: OR < AND < NOT < comparison < primary
def parse_or(self): # Lowest precedence
left = self.parse_and()
while self._current().type == 'OR':
self._consume()
left = OrExpression(left, self.parse_and())
return left
def parse_and(self): # Higher precedence than OR
left = self.parse_not()
while self._current().type == 'AND':
self._consume()
left = AndExpression(left, self.parse_not())
return left
def parse_not(self): # Higher precedence than AND
if self._current().type == 'NOT':
self._consume()
return NotExpression(self.parse_not())
return self.parse_primary()
This naturally enforces NOT before AND before OR without any explicit precedence table.
Q6: How do I protect against infinite loops or infinite recursion in interpreted code?
Add a depth or step counter to the Context and abort evaluation when limits are exceeded:
class Context:
def __init__(self, data: dict, max_depth: int = 100, max_steps: int = 10_000):
self._vars = data
self._depth = 0
self._steps = 0
self._max_depth = max_depth
self._max_steps = max_steps
def enter(self) -> None:
self._depth += 1
self._steps += 1
if self._depth > self._max_depth: raise RecursionError("Expression depth limit exceeded")
if self._steps > self._max_steps: raise TimeoutError("Evaluation step limit exceeded")
def exit(self) -> None:
self._depth -= 1
Q7: How do I cache or memoize repeated sub-expression evaluations?
Wrap expressions in a memoizing decorator that caches the result for a given context state:
class MemoizedExpression(Expression):
def __init__(self, inner: Expression) -> None:
self._inner = inner
self._cache: dict = {}
def interpret(self, ctx: Context) -> float:
# Use a hashable snapshot of the relevant context state as the cache key
key = ctx.snapshot_hash()
if key not in self._cache:
self._cache[key] = self._inner.interpret(ctx)
return self._cache[key]
This is especially valuable in rule engines where the same sub-expression (e.g., country == "US") appears in multiple rules evaluated against the same context.
Q8: What is the difference between Interpreter and Strategy for evaluating rules?
Strategy selects one algorithm from a fixed set of interchangeable options. You'd use Strategy if you have three different pricing strategies and want to pick one at runtime. It doesn't compose — you can't combine strategies into strategy1 AND strategy2.
Interpreter builds composable expressions from a grammar. You can combine them arbitrarily: rule1 AND (rule2 OR NOT rule3). The power of Interpreter is in the recursive composition — expressions nest inside each other without limit.
Use Strategy for "which algorithm" decisions. Use Interpreter for "build a sentence in a language" situations.
Q9: How do I serialize and deserialize an AST for storage or transmission?
Each expression node should support serialization to a dictionary or JSON:
def to_dict(self) -> dict:
return {
"type": "AndExpression",
"left": self._left.to_dict(),
"right": self._right.to_dict()
}
# Registry for deserialization
EXPR_TYPES = {
"NumberExpression": NumberExpression,
"VariableExpression": VariableExpression,
"AndExpression": AndExpression,
# ...
}
def from_dict(data: dict) -> Expression:
cls = EXPR_TYPES[data["type"]]
return cls.from_dict(data)
This allows rules to be stored in a database, transmitted over a network, and reconstructed on any node — essential for distributed rule engines and persistent configuration systems.
Q10: When is the Interpreter Pattern NOT appropriate?
Avoid Interpreter when:
- Performance is critical: Tree traversal with virtual dispatch is slow compared to compiled bytecode or native evaluation. For hot paths, compile the AST to bytecode or native code instead.
- The grammar is large and complex: More than ~20–30 grammar rules means hundreds of classes — consider a parser generator + Visitor instead.
- The language is a well-known standard: Don't reimplement SQL, JSON, or regex from scratch — use existing libraries that are battle-tested and optimized.
- The team isn't familiar with formal grammars: The pattern requires clear grammar design; ad-hoc "interpreters" without a formal grammar quickly become unmaintainable.
- Simple string substitution suffices: If your "DSL" is just variable substitution in a template string, a
str.format()call is the right tool.
Key Takeaways
🎯 Core Concept: The Interpreter Pattern represents each rule of a formal grammar as a class. Sentences in that grammar are built as trees of expression objects (the AST), and evaluating a sentence means calling interpret(context) on the root — which recursively calls interpret() on each node down the tree. The Context carries shared state (variable bindings, environment) across the entire traversal.
🔑 Key Benefits:
- Safe DSLs: Evaluate domain-specific languages without the security risks of
eval() - Extensibility: New grammar rules are new classes — existing expressions are untouched
- Composability: Complex expressions are composed recursively from simpler ones using the same interface
- Testability: Every expression class is independently testable with known inputs
- Readability: Code mirrors grammar —
AndExpression,OrExpression,FieldComparisonread like the language they implement - Reusable ASTs: Build the AST once, evaluate it against many contexts efficiently
🌐 Language-Specific Highlights:
- Python: Use
ABCand@abstractmethodto enforce the interface; useall()/any()for flat multi-operand AND/OR to avoid recursion depth issues; never useeval()for user input - TypeScript: Use discriminated unions or typed generics for type-safe ASTs; enforce strict equality in comparisons; make the entire evaluation chain
asyncif any expression is async - Java: Use
recordfor immutable expression nodes — perfect value semantics for AST nodes; always use&&and||(not&and|) for short-circuit evaluation; eliminate left recursion in parsers - JavaScript: Use
Mapinstead of plain objects for the Context to prevent prototype pollution;Object.freeze()expression nodes to make them safely shareable flyweights - C#: Use
recordtypes for immutable, structurally comparable expression nodes; cache compiled ASTs inConcurrentDictionaryfor repeated evaluation; useasync Taskif any evaluation is async - PHP: Whitelist operators in a closure map — never use them as raw strings in
eval(); usearray_key_exists()(notisset()) for nullable context values - Go: Always return and propagate errors from
Interpret()— never discard with_; implementfmt.Stringeron every expression type for readable debugging; use function maps for operator dispatch - Rust: Use
enumfor closed expression sets (exhaustive matching, no heap allocation per variant); use&strfor context lookups to avoid clone overhead;?operator for clean error propagation - Dart: Use
sealed class(Dart 3+) for exhaustive pattern matching over expression types; leverageswitchexpressions for concise interpreter implementations - Swift: Use
structorindirect enumfor expression nodes (value semantics, no accidental sharing); usethrows+ typed errors for meaningful failure propagation - Kotlin: Use
sealed classfor exhaustivewhenexpressions — compiler enforces that all subtypes are handled; use operator overloading (+,*,-,/) to create an elegant internal DSL for building ASTs
📋 When to Use Interpreter:
- You need to evaluate sentences in a domain-specific language (query language, rule engine, config DSL, formula evaluator)
- The grammar is well-defined and relatively small (under ~20–30 rules)
- New grammar rules will be added over time and you want to do so without touching existing code
- You need a safe sandbox for user-defined logic (as opposed to
eval()) - The same AST will be evaluated against many different contexts (build once, evaluate many)
- You need to serialize, store, and replay expressions (event sourcing, persistent rules)
⚠️ When NOT to Use Interpreter:
- The grammar is large or complex — use a parser generator (ANTLR, PLY, PEG.js) instead
- Performance is the primary concern — compile to bytecode or use an existing optimized engine
- You're reimplementing a well-known language (SQL, JSON, regex) — use the battle-tested libraries
- Simple string substitution or a single fixed computation is all you need — the pattern's overhead isn't justified
- The team is unfamiliar with formal grammar concepts — the pattern requires deliberate grammar design
🛠️ Best Practices Across Languages:
- Design the grammar first: Write it out formally (BNF or similar) before writing a single class — the grammar IS the design
- Separate parsing from evaluation: The parser builds the AST; the AST evaluates itself. Never mix them
- Keep the Context immutable during evaluation: Expressions should be pure functions of their children and the context — side effects in expressions cause hard-to-debug bugs
- Cache the AST: Parse expensive rule strings once; cache the AST and evaluate it against many contexts
- Add a depth/step limit to the Context: Protect against infinite recursion or runaway evaluation
- Use sealed/exhaustive types:
sealed class(Kotlin/Dart),sealedinterface (Java 17+),indirect enum(Rust/Swift) ensure every expression type is handled everywhere - Implement a readable
toString(): AST nodes should print as the expressions they represent — essential for debugging and logging - Whitelist all operators: Never pass operator strings from user input to native comparisons or eval — always validate against a known safe set
💡 Common Pitfalls:
eval()for user input: The fast but catastrophically insecure shortcut — always use a proper interpreter- Mutable expression nodes: Shared subtrees can be corrupted if nodes are mutable — make them immutable
- Unbounded recursion depth: Deep ASTs cause stack overflows — add depth limits or use iterative traversal for flat structures
- Missing short-circuit evaluation: AND/OR expressions should short-circuit — evaluating all branches wastes work and can cause errors in the skipped branch
- No error propagation: Every
interpret()call can fail (undefined variable, division by zero) — errors must propagate faithfully up the tree - Left-recursive grammars in recursive-descent parsers: Will loop infinitely — rewrite as iterative left-fold
- Prototype pollution via plain object context: Use
Mapin JavaScript anddictwithinchecks in Python, never rely onobj[name]which traverses the prototype chain
🎁 Advanced Techniques:
- Visitor + Interpreter: Use Interpreter for the AST structure; use Visitor for multiple independent operations (evaluate, type-check, optimize, pretty-print) on the same AST without modifying node classes
- Compiled Interpretation: Walk the AST once to generate bytecode or a function; subsequent evaluations run the compiled form at native speed — the approach used by most production rule engines
- Memoized Sub-Expressions: Wrap frequently evaluated sub-expressions in a memoizing decorator that caches results keyed on context state
- Operator Precedence Climbing: A more efficient alternative to recursive descent for expression parsers — handles precedence and associativity in a single iterative loop
- Incremental Evaluation: Track which variables changed between context updates and only re-evaluate the sub-trees that depend on those variables — essential for reactive spreadsheet-like systems
- AST Optimization Pass: Before first evaluation, walk the AST and apply constant folding (
3 + 4→7), dead branch elimination (TRUE AND x→x), and other optimizations — reduces work at evaluation time
The Interpreter Pattern is the foundational block for any system that evaluates a language — from the simplest formula field in a configuration file to the query engine of a full database. When you need to evaluate expressions safely, extensibly, and composably, the Interpreter Pattern gives you the architecture to do it right.
Want to dive deeper into design patterns? Check out our comprehensive guides on:
- Visitor Pattern
- Composite Pattern
- Command Pattern
- Chain of Responsibility Pattern
- Strategy Pattern Each guide includes examples in all 11 programming languages with language-specific best practices.