Interpreter Pattern: Defining a Grammar and Interpreting Language Rules
Some problems require you to evaluate or process sentences, expressions, or commands that follow a particular grammar or set of rules.
While you could build a big
It’s especially useful when you have:
- A fixed grammar that’s not too large.
- The need to process simple scripting languages, filters, or rules inside your application.
Real-World Analogy
Think of Google Translate.
When you enter a sentence in another language, the system breaks it down into smaller pieces (words, grammar rules) and interprets each piece according to a defined grammar before producing the final translation.
In the Interpreter Pattern:
- AbstractExpression defines the interpretmethod.
- TerminalExpression handles the simplest elements.
- NonTerminalExpression handles rules composed of other expressions.
Real-World Backend Example
Let’s say you’re building a backend system where admins can filter logs using a custom mini-language:
- "ERROR AND NOT DEBUG"means: show entries with levelERRORbut notDEBUG.
- "INFO OR WARNING"means: show entries with levelINFOorWARNING.
Instead of writing a hardcoded parser, we can model this with the Interpreter Pattern.
Abstract Expression
from abc import ABC, abstractmethod class Expression(ABC): @abstractmethod def interpret(self, context): pass
Terminal Expression
class TerminalExpression(Expression): def __init__(self, data): self.data = data def interpret(self, context): return self.data in context
Non-Terminal Expression
class AndExpression(Expression): def __init__(self, expr1, expr2): self.expr1 = expr1 self.expr2 = expr2 def interpret(self, context): return self.expr1.interpret(context) and self.expr2.interpret(context) class OrExpression(Expression): def __init__(self, expr1, expr2): self.expr1 = expr1 self.expr2 = expr2 def interpret(self, context): return self.expr1.interpret(context) or self.expr2.interpret(context) class NotExpression(Expression): def __init__(self, expr): self.expr = expr def interpret(self, context): return not self.expr.interpret(context)
Example usage
if __name__ == "__main__": # Define rules error = TerminalExpression("ERROR") debug = TerminalExpression("DEBUG") info = TerminalExpression("INFO") warning = TerminalExpression("WARNING") error_and_not_debug = AndExpression(error, NotExpression(debug)) info_or_warning = OrExpression(info, warning) # Test contexts log1 = {"ERROR", "API"} log2 = {"DEBUG", "API"} log3 = {"INFO", "SERVER"} log4 = {"WARNING", "DB"} print("Log1 matches ERROR AND NOT DEBUG?", error_and_not_debug.interpret(log1)) print("Log2 matches ERROR AND NOT DEBUG?", error_and_not_debug.interpret(log2)) print("Log3 matches INFO OR WARNING?", info_or_warning.interpret(log3)) print("Log4 matches INFO OR WARNING?", info_or_warning.interpret(log4))
Output:
Log1 matches ERROR AND NOT DEBUG? True Log2 matches ERROR AND NOT DEBUG? False Log3 matches INFO OR WARNING? True Log4 matches INFO OR WARNING? True
Gotchas & Considerations
- Small Grammar Only
- The Interpreter Pattern works best for small, stable grammars. Large grammars become complex and hard to manage.
- Performance
- If you need to interpret expressions many times, consider compiling them into a faster form.
- Readability
- While it models grammar clearly, the number of classes can grow quickly.
- Alternatives
- For large or dynamic grammars, use parser generators (like plyorlarkin Python) instead.
- For large or dynamic grammars, use parser generators (like
When to Use Interpreter Pattern
- You have a fixed set of grammar rules to represent and interpret.
- You need a custom DSL (Domain-Specific Language) inside your application.
- You want a clean object-oriented way to process structured expressions.
Final Thoughts
The Interpreter Pattern gives you a structured, class-based way to parse and execute small, domain-specific languages without writing a giant parser.
It shines when the grammar is small, well-defined, and unlikely to change frequently.
For large, evolving languages, the number of classes can explode and performance can drop — in those cases, dedicated parsing libraries or tools are a better fit.
When used in the right context, Interpreter can turn complex expression handling into clean, composable logic that’s easy to extend.