Easy Patterns in Python: The Interpreter

Introduction

The Interpreter pattern can be used to interpret and evaluate sentences in a language. The idea is to make a class for each symbol, both terminal and non-terminal and construct a syntax tree which can be evaluated and interpreted.

The interpreter pattern I will use here is probably one of the simplest implementations of this pattern. Before you use this pattern, what you need is a well-defined grammar for your language so that it can be evaluated and interpreted.

The simplest form of the interpreter looks like this:

This is not the whole picture, as we also need to build a syntax tree, which can be done in a parser, which is outside the scope of this article.

I think the idea will become much clearer when we implement this pattern.

Implementation in Python

The first thing we need to do is import these modules:

from abc import ABC, abstractmethod

What are these?

  • ABC stands for ‘Abstract Base Class’, that is a class which can contain one more abstract methods.
  • An abstract method, denoted by the @abstractmethod attribute is a method which is not implemented in the Abstract Base Class but in a subclass of that class.

We will start with the Expression base class:

class Expression(ABC):
    @abstractmethod
    def interpret(self, context: dict) -> bool:
        pass

All this class does is interpret an expression, which in our case will return a boolean.

Now, since we are returning booleans, we can define an AndExpression:

class AndExpression(Expression):
    def __init__(self, left: Expression, right: Expression):
        self._left_expression = left
        self._right_expression = right

    def interpret(self, context: dict) -> bool:
        return self._left_expression.interpret(context) and self._right_expression.interpret(context)

Two notes:

  1. In the constructor we pass two expressions to the constructor
  2. The interpret() method just interprets the expression and performs an and-operation. An and-operation, as you may know, returns true if both operands are true

The OrExpression is built in a similar way. Note that an or-operation is true if either one operand is true or both are true.

class OrExpression(Expression):
    def __init__(self, left: Expression, right: Expression):
        self._left_expression = left
        self._right_expression = right

    def interpret(self, context: dict) -> bool:
        return self._left_expression.interpret(context) or self._right_expression.interpret(context)

To make are language at bit more expressive we also need a NotExpression:

class NotExpression(Expression):
    def __init__(self, expression: Expression):
        self._expression = expression

    def interpret(self, context: dict) -> bool:
        return not self._expression.interpret(context)

Most computer languages have the concept of variables. Here we have an expression which looks up a boolean variable in the provided context. Note that if the variable can not be found, it reverts to False:

class VariableExpression(Expression):
    def __init__(self, variable_name: str):
        self._variable_name = variable_name

    def interpret(self, context: dict) -> bool:
        return context.get(self._variable_name, False)

Finally we define a ComparisonExpression which compares its value to a value from the context:

class ComparisonExpression(Expression):
    """Terminal expression that compares a variable's value in the context."""
    def __init__(self, variable_name: str, operator: str, value):
        self._variable_name = variable_name
        self._operator = operator
        self._value = value

    def interpret(self, context: dict) -> bool:
        actual_value = context.get(self._variable_name)
        if actual_value is None:
            return False
        
        if self._operator == "==":
            return actual_value == self._value
        elif self._operator == "!=":
            return actual_value != self._value
        elif self._operator == ">":
            return actual_value > self._value
        elif self._operator == "<":
            return actual_value < self._value
        elif self._operator == ">=":
            return actual_value >= self._value
        elif self._operator == "<=":
            return actual_value <= self._value
        else:
            return False

Time to test:

We can now test this setup by giving three examples:

if __name__ == "__main__":
    # Example 1
    print("=== Example 1: Access Control System ===")
    context = {
        "is_authenticated": True,
        "is_admin": False,
        "is_premium_user": True
    }
    
    auth_check = VariableExpression("is_authenticated")
    admin_check = VariableExpression("is_admin")
    premium_check = VariableExpression("is_premium_user")
    
    # Rule: User must be authenticated AND (admin OR premium)
    admin_or_premium = OrExpression(admin_check, premium_check)
    full_access = AndExpression(auth_check, admin_or_premium)
    
    print(f"Context: {context}")
    print(f"Has full access: {full_access.interpret(context)}")
    print()
    
    # Example 2
    print("=== Example 2: Age and Status Validation ===")
    context2 = {
        "age": 25,
        "status": "active",
        "country": "USA"
    }
    
    age_check = ComparisonExpression("age", ">=", 18)
    status_check = ComparisonExpression("status", "==", "active")
    country_check = ComparisonExpression("country", "==", "USA")
    
    # Rule: Age >= 18 AND status == active AND country == USA
    eligible = AndExpression(AndExpression(age_check, status_check), country_check)
    
    print(f"Context: {context2}")
    print(f"Is eligible: {eligible.interpret(context2)}")
    print()
    
    # Example 3
    print("=== Example 3: Complex Business Rule ===")
    context3 = {
        "is_authenticated": True,
        "is_banned": False,
        "account_age_days": 45
    }
    
    auth_expr = VariableExpression("is_authenticated")
    banned_expr = VariableExpression("is_banned")
    age_expr = ComparisonExpression("account_age_days", ">=", 30)
    
    # Rule: authenticated AND NOT banned AND account_age >= 30
    not_banned = NotExpression(banned_expr)
    can_post = AndExpression(AndExpression(auth_expr, not_banned), age_expr)
    
    print(f"Context: {context3}")
    print(f"Can post content: {can_post.interpret(context3)}")

Some notes:

  • The first example is for an access control system, and the context just consists of boolean variables. We start by defining three types of type VariableExpression. Next we use or-expressions and and-expressions to express the rule, and print out the result.
  • The second example is a status and age validation, here we only use the ComparisonExpression as the basis and combine with with a nested AndExpression.
  • The third and final example defines a more complex business role, combining some VariableExpressions with one ComparisonExpression and again combining them using a NotExpression and a nested AndExpression.

Conclusion

The Interpreter pattern, as demonstrated in this article, provides a clean and extendable way to evaluate expressions defined in a specific grammar. We saw how the core idea revolves around creating a class hierarchy where each terminal (like VariableExpression and ComparisonExpression) and non-terminal (like AndExpression, OrExpression, and NotExpression) rule of the grammar is represented by its own class. This structure naturally forms a syntax tree (or Abstract Syntax Tree – AST), which is then traversed and evaluated using the recursive interpret() method.

The implementation in Python using Abstract Base Classes (ABC) ensures that all expressions adhere to a common interface, promoting flexibility. While this example focused on a simple boolean logic and comparison language, the Interpreter pattern’s utility extends to more complex scenarios, such as creating basic query languages, defining business rule engines, or building regular expression parsers.

The power of this pattern lies in its ease of extension. To add a new operation (e.g., an XOR expression or a string concatenation), you simply create a new subclass of Expression. By externalizing the language grammar into object-oriented classes, you create a more maintainable and easily modifiable system compared to using large conditional statements. Understanding this pattern is a valuable asset for any developer looking to build systems that need to process and act upon structured input.

The Code Nomad
The Code Nomad
Articles: 167

Leave a Reply

Your email address will not be published. Required fields are marked *