Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work

Interpreter

Represent the rules of a small language as a tree of objects, and evaluate the tree. This is useful for filters, pricing rules and other mini-languages.

The problem

A marketing team wants to configure discount rules without a deploy, for example: "10% off if the cart total is over 100 and the customer is a member, or the country is NZ". Developers keep hard-coding each new rule, and every campaign waits for a release.

The rules are really a tiny language with comparisons, and, or and not. You need a way to represent and evaluate expressions in it.

Real-world analogy

Musical notation. A score is a structured "sentence" in a small language; a musician (the interpreter) reads each symbol and turns it into sound. The same musician can play any score written in that language.

Without the pattern

def eligible(cart):
    # a new campaign = a new hard-coded condition and a new deploy
    return (cart.total > 100 and cart.member) or cart.country == "NZ"

The pattern

  • Abstract expression: an interface with interpret(context).
  • Terminal expressions: leaves of the grammar, such as numbers, variables and comparisons (GreaterThan("total", 100)).
  • Non-terminal expressions: rules made of other expressions (And(left, right), Or, Not). They interpret their children and combine the results.
  • Context: the input being evaluated (the cart).

The expression tree is a Composite; Interpreter adds the meaning. A parser (separate from the pattern) usually turns text into the tree.

The code

The rule is built as a tree of expression objects and evaluated against three carts. A describe() method turns the tree back into readable text.

A tiny rule language for discountsTry it yourself
class Var:  # terminal: reads a value from the context
    def __init__(self, name):
        self.name = name

    def interpret(self, ctx):
        return ctx[self.name]

    def describe(self):
        return self.name


class Const:
    def __init__(self, value):
        self.value = value

    def interpret(self, ctx):
        return self.value

    def describe(self):
        return f"'{self.value}'" if isinstance(self.value, str) else str(self.value)


class Gt:
    def __init__(self, left, right):
        self.left, self.right = left, right

    def interpret(self, ctx):
        return self.left.interpret(ctx) > self.right.interpret(ctx)

    def describe(self):
        return f"{self.left.describe()} > {self.right.describe()}"


class Eq(Gt):
    def interpret(self, ctx):
        return self.left.interpret(ctx) == self.right.interpret(ctx)

    def describe(self):
        return f"{self.left.describe()} == {self.right.describe()}"


class And:  # non-terminal: combines other expressions
    def __init__(self, left, right):
        self.left, self.right = left, right

    def interpret(self, ctx):
        return self.left.interpret(ctx) and self.right.interpret(ctx)

    def describe(self):
        return f"({self.left.describe()} and {self.right.describe()})"


class Or(And):
    def interpret(self, ctx):
        return self.left.interpret(ctx) or self.right.interpret(ctx)

    def describe(self):
        return f"({self.left.describe()} or {self.right.describe()})"


rule = Or(And(Gt(Var("total"), Const(100)), Eq(Var("member"), Const("yes"))),
          Eq(Var("country"), Const("NZ")))
print("rule:", rule.describe())

carts = [{"total": 150, "member": "yes", "country": "US"},
         {"total": 150, "member": "no", "country": "US"},
         {"total": 20, "member": "no", "country": "NZ"}]
for cart in carts:
    print(cart["total"], cart["member"], cart["country"], "->", "discount" if rule.interpret(cart) else "no discount")

Output

rule: ((total > 100 and member == 'yes') or country == 'NZ')
150 yes US -> discount
150 no US -> no discount
20 no NZ -> discount

In a real system the rule would come from a database or an admin screen as text, and a small parser would build this tree. The interpreter part, the tree evaluating itself, stays the same.

When to use it

  • You have a small, stable language that users or configuration need to express: discount and pricing rules, search filters (status:open author:ada), feature-flag targeting, alert conditions, permission policies.
  • The grammar is simple enough that one class per rule is manageable.

When not to use it

  • The language is large or complex. Use a parser generator (ANTLR, Lark, PEG.js) or an existing engine instead.
  • Performance matters for heavy evaluation. Tree-walking interpreters are slow compared with compiled code.
  • You could use an existing safe expression language (JSONLogic, CEL, a SQL WHERE clause) instead of inventing one.
  • Never interpret untrusted input with eval(). A small interpreter with a fixed set of operations is the safe alternative.

You've already used it

  • Regular expressions are a mini-language compiled to an internal tree or automaton.
  • SQL WHERE clauses, Elasticsearch query DSL and MongoDB query documents are expression trees evaluated by an engine.
  • Template engines (Jinja, Blade, Handlebars) parse templates into trees and interpret them.
  • Feature-flag rules and policy engines (Open Policy Agent, AWS IAM policy evaluation).

Key points

  • Interpreter represents each grammar rule as a class and evaluates a tree of expressions against a context.
  • The tree is a Composite; a separate parser usually builds it from text.
  • Use it for small, stable rule languages; use real parsers or existing expression engines for anything bigger.
  • It is a safe alternative to eval() for user-defined rules.
  • Further reading: Refactoring Guru: design pattern catalog and Interpreter pattern.

Practice questions

What does the Interpreter pattern do?

Answer

It represents a small language's grammar as classes: terminal expressions (numbers, variables) and non-terminal expressions (and, or, comparisons) that combine others. A sentence becomes a tree of these objects, and calling interpret(context) on the root evaluates it.

When should you not use the Interpreter pattern?

Answer

When the language is large or complex (use a parser generator or an existing engine), when evaluation performance is critical, or when an existing expression language (SQL, CEL, JSONLogic) already does the job.

Why is a small interpreter safer than `eval()` for user-defined rules?

Answer

eval() runs arbitrary code, so a malicious rule can do anything the program can. A small interpreter supports only a fixed set of operations (comparisons, and/or), so user input can't escape that sandbox.

Open in the app to mark lessons complete, take notes and track your progress.