DEV Community

Cover image for How to write a basic rule engine in Python
Scott Rallya
Scott Rallya

Posted on • Edited on

How to write a basic rule engine in Python

While there are many existing rules engines in Python, such as the amazing rule-engine framework, I thought it would be an interesting exercise to utilize ChatGPT to help me write my own basic rule engine from scratch. So, without further ado - I present to you PYROSE - PYthon Rule-based Operation System Engine.

The basic structure of our rule system is as follows. At the basic foundation of engine we have Facts. Facts are simple objects that represent information we want stored in our system and can contain any kind of information that is relevant to the design of the constraints by which our rules-engine operates. Let us begin by defining a very simple structure for our Facts

from typing import Any

class Fact:
    def __init__(self, **kwargs: Any):
        self.__dict__.update(kwargs)
Enter fullscreen mode Exit fullscreen mode

Here we define a very simple object that can be instantiated with any number of keyword arguments to define our fact. We then update our object used the self.__dict__.update method to add attributes to our object corresponding to the keywords passed to the initializer. We can initialize a fact as follows,

person_fact = Fact(name="John Brown", age="35", occuptation="Software Developer")
Enter fullscreen mode Exit fullscreen mode

Accessing a attribute is as simple using dot notation as if you were access a member or method on the object.

person_fact.age # Returns 35
Enter fullscreen mode Exit fullscreen mode

Now that we have established our Fact object, we need to define a Condition by which to apply our Fact object. Our Conditions are very simple in design as well to start off with. The initializer takes two parameters, a name and an evaluation function. The evaluation function will be applied to a Fact and return a bool. It also contains a method, evaluate, which will take a Fact and return a bool, calling the objects evaluation_function on the Fact. Here is the outline of our Condition class:

from rule_engine.fact import Fact

from typing import Callable, Any, Dict, List

class Condition:
    def __init__(self, name: str, evaluation_function: Callable[[Fact], bool]):
        self.name = name
        self.eval_func = evaluation_function

    def evaluate(self, fact: Fact) -> bool:
        return self.eval_func(fact)
Enter fullscreen mode Exit fullscreen mode

Following a similar suit, we can define our Action class. An Action class takes in a name parameter and a Callable parameter into its initializer. The Callable corresponds to the Action's execution function, what is executed if all the Conditions of the Rule are True. It also contains an execute method which calls the execute function on the given Fact, giving a None type as a result. Here is the definition of the Action class:


from typing import Callable, Any, Dict, List
from rule_engine.fact import Fact

class Action:

    def __init__(self, name: str, execution_function: Callable[[Fact], None]):
        self.name = name
        self.exec_func = execution_function

    def execute(self, fact: Fact) -> None:
        self.exec_func(fact)
Enter fullscreen mode Exit fullscreen mode

So far we've defined our Fact, our Condition, and our Action. We can combine these together to form our Rule class. This will be the main driving force behind our Engine. The Rule class will initially be constructed with a single Action and Condition. Two methods, add_condition and add_action, will allow you to add additional conditions and actions to the Rule as you need.

Finally, a third method, evaluate, will take in a list of Facts. It defines a fact_generator which takes a list of conditions and a list of facts. For each fact, it maps each of the conditions' eval_func against the fact. It then reduces this list to a single boolean value, and if this value is true, we yield the fact.

We then call the fact_generator function, wrap it in a list to get the list of all True facts, and if the length is greater than 0, we iterate through the list of true facts. For each true fact, we iterate through a list of the rules actions and call the action's exec_func on the true fact.

The complete definition of the Rule class is as follows:


from rule_engine.condition import Condition
from rule_engine.action import Action
from rule_engine.fact import Fact

from typing import Any, List
from functools import reduce

class Rule:
    def __init__(self, condition: Condition, action: Action):
        self.conditions = [condition]
        self.actions = [action]

    def add_condition(self, condition: Condition) -> None:
        self.conditions.append(condition)

    def add_action(self, action: Action) -> None:
        self.actions.append(action)

    def evaluate(self, facts: List[Fact]) -> Any:
        def fact_generator(conditions: List[Condition], facts: List[Fact]):
            all_conditions_true = True
            for fact in facts:
                results = map(lambda condition: condition.eval_func(fact), conditions)
                all_conditions_true = reduce(lambda x, y: x and y, results)

                if all_conditions_true:
                    yield fact

        true_facts = list(fact_generator(self.conditions, facts))

        if len(true_facts) > 0:
            for fact in true_facts:
                for action in self.actions:
                    action.exec_func(fact)
Enter fullscreen mode Exit fullscreen mode

You can use the rule engine as follows:


from rule_engine.fact import Fact
from rule_engine.condition import Condition
from rule_engine.action import Action
from rule_engine.rule import Rule

age_cond = Condition(name="Age>=21", evaluation_function=lambda fact: fact.age >= 21)
occupation_cond = Condition(name="Occupation==Software Developer", evaluation_function=lambda fact: fact.occupation == "Software Developer")

print_action = Action(name="Print Fact", execution_function=lambda fact: print("Name: {} Age: {} Occupation: {}".format(fact.name, fact.age, fact.occupation)))

john = Fact(age=25,name="John Brown", occupation="Software Developer")
sarah = Fact(age=35,name="Sarah Purple", occupation="Data Engineer")
barry = Fact(age=27, name="Barry White", occupation="Software Developer")

rule = Rule(condition=age_cond, action=print_action)
rule.add_condition(occupation_cond)

rule.evaluate([john, sarah, barry])
Enter fullscreen mode Exit fullscreen mode

All in all, this is a very basic demo of a very simplistic Rules engine. However, I think it can definitely be built and improved upon in any number of ways, so I hope that you enjoyed reading and using what you learned in your own projects. If you have any questions, comments, suggestions or ideas, please feel free to reach out to me. Thank you for reading.

Acknowledgments:

Thanks to John for catching some unnecessary code duplication! Catch out John's articles.

Top comments (3)

Collapse
 
cosmo102 profile image
chi cosmo

Nice article. Looks like the "evaluate" method in Condition and the "execute" method in Action are unused.

Collapse
 
rouilj profile image
John P. Rouillard

Interesting article. In your "complete definition of the Rule class" it looks like you accidentally duplicated your imports, or was that intentional for some reason?

Collapse
 
fractalis profile image
Scott Rallya

Thanks for catching that code duplication; completely unintentional. I hope you don't mind, I threw up an acknowledgement and a link to your profile at the end of the article. I appreciate it, thank you!