rule-engine matches Python objects with rules written as strings
A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.
At a glance
- What is it?
- rule-engine is a Python library with its own expression language, optional type hints and rule chains, built for filtering arbitrary Python objects by a rule the user can type. It suits tools that expose filtering to non-programmers.
- Who is it for?
- Adopt rule-engine when you need to let users describe a filter in text and apply it to Python objects, for example in a security tool, a log analyser or a webhook filter, and you want the expression language to be its own thing rather than eval. Do not adopt it if you control both sides of the contract, since a plain Python callable is simpler and faster.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Rules as strings, evaluated against plain Python objects
The library is described as a lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects. The key idea is that a rule is a string, not a Python expression you build by concatenation. You write something like a condition on a first name and a regular expression on an email, hand it to the library, and call matches with a dictionary. The syntax is described as most similar to Python with some inspiration from Ruby, and the language features include optional type hinting, matching strings with regular expressions, datetime datatypes, compound datatypes that are equivalents for the Python dict, list and set types, data attributes, thread safety, and rule chains that return the value of the first matching rule. The audience is developers building tools where the end user types the filter, which is the situation where handing someone eval would be a security problem and where a constrained grammar is a feature rather than a limitation.
The basic usage, which is also the whole mental model
The example in the documentation defines a rule and applies it to two dictionaries, showing one match and one non-match:
import rule_engine
# match a literal first name and applying a regex to the email
rule = rule_engine.Rule(
'first_name == "Luke" and email =~ ".*@rebels.org$"'
) # => <Rule text='first_name == "Luke" and email =~ ".*@rebels.org$"' >
rule.matches({
'first_name': 'Luke', 'last_name': 'Skywalker', 'email': '[email protected]'
}) # => True
rule.matches({
'first_name': 'Darth', 'last_name': 'Vader', 'email': '[email protected]'
}) # => FalseThree things are worth noticing in those lines. The equality operator is == as in Python, but the regular expression operator is =~, which is not a Python operator at all, so the grammar is genuinely its own thing rather than a Python subset. The rule object carries its own text and can be inspected, which matters when you want to log what filter a user actually applied. And the matching call takes a plain dictionary, so the target of a rule is any ordinary Python object rather than a special wrapper. The examples that ship with the library, csv_filter.py, database.py, github_filter.py and a shodan/ directory, indicate the intended use cases, which are filtering records from a CSV, from a database or from an API response, and doing so with expressions that a user could type.
The type system turns typos into exceptions
The optional type system is the feature that changes how the library behaves in a typed context. You create a custom context that defines the symbols, and the documentation gives a context with two symbols, one string and one float:
import rule_engine
# define the custom context with two symbols
context = rule_engine.Context(type_resolver=rule_engine.type_resolver_from_dict({
'first_name': rule_engine.DataType.STRING,
'age': rule_engine.DataType.FLOAT
}))
# receive an error when an unknown symbol is used
rule = rule_engine.Rule('last_name == "Vader"', context=context)
# => SymbolResolutionError: last_nameBecause the symbols are defined, an exception is raised if an unknown symbol is specified or an invalid operation is used. An unknown symbol produces a SymbolResolutionError naming the symbol, and an operation that does not type check produces an EvaluationError described as a data type mismatch, for instance on the expression first_name + 1. That is the practical value of the type system. Without it, a rule with a typo silently evaluates to false against every object, and a user filtering a security log has no way of knowing their filter is broken. With it, the failure happens at rule construction or evaluation and it names the offending symbol. The trade is that the context has to be built to match the shape of the data, which is extra work for a library whose selling point is that it is lightweight.
A REPL for experimenting with the expression syntax
The documentation makes a point of the developer experience for a language that is not Python. There is a Debug REPL, described as making experimentation easy, and after installing you just run it as a module:
python -m rule_engine.debug_replFor an expression language, a REPL is not a convenience, it is the primary learning tool, because the failure modes of an unfamiliar grammar are mostly discoverable by trying things and reading the errors. The documentation says that after installing you just run the module with `python -m rule_engine.debug_repl`. The exception types named in the documentation, SymbolResolutionError and EvaluationError, are the sort of message that teaches you the rules as you go. The language also supports datetime datatypes, which means the expressions can reason about time, and compound datatypes that stand in for the Python dict, list and set types, so a rule can express membership or key access rather than only scalar comparisons. Data attributes give access to attributes of the matched object. And rule chains, which return the value of the first matching rule, are how you build a cascade of fallbacks, for example a default value applied when nothing else matched. The library is also documented as thread safe, which matters if you evaluate rules from a server that handles requests concurrently.
What the language deliberately cannot do
The design of a constrained grammar is that it does not do everything, and the value of the library depends on where the boundary sits. The grammar is a condition language over attributes and symbols, not a general programming language, so you cannot write a loop, call a function on a matched object, or reach outside the data. That is a feature when the expressions come from users, because a user typing into your filter box cannot execute arbitrary code, and it is a limitation when you want the filter to compute something. The optional type system makes the boundary explicit, which is the right design, but it also means the context has to be maintained alongside the data model, and a schema change that renames a field breaks every rule that referenced it. The rule chains feature is the escape hatch for anything beyond a single condition, but the chaining is still declarative rather than procedural, so a complex decision still has to be expressed as an ordered list of conditions. The realistic use is filtering and classification, not computation, and a project that needs computation should be using Python directly and a callable interface instead.
Version support is moving, and the licence is permissive
The release history shows active work with some churn. The current version is 5.1.0, published on 2026-10-20, following 5.0.2 on 2026-07-08 and 5.0.1 on 2026-06-25, and the last push to the repository was on 2026-10-20. Two warnings in the documentation matter for planning. Version 6.0 will remove support for Python 3.10, and there is currently no timeline for its release, so if you are on 3.10 you have a known deprecation ahead. And version 5.0 removed support for Python 3.6, 3.7, 3.8 and 3.9 and deprecated attribute style access on MAPPING values, with bracket syntax such as mapping["key"] to be used instead, with the change log holding the details. The current metadata requires Python 3.10 or newer and lists support through 3.13. The only runtime dependency is python-dateutil, and the project is classified as Production/Stable. The licence is the BSD 3-Clause, and the documentation states plainly that the library is usable for both commercial and private purposes. The repository has a lib/ directory rather than a package directory, along with docs/, examples/, tests/, a tasks.py for invoke, a uv.lock, and a pyproject.toml with mypy configured over lib/rule_engine.
Editorial conclusion
Adopt rule-engine when you need to let users describe a filter in text and apply it to Python objects, for example in a security tool, a log analyser or a webhook filter, and you want the expression language to be its own thing rather than eval. Do not adopt it if you control both sides of the contract, since a plain Python callable is simpler and faster. Before you upgrade from 4.x, read the change log, because version 5.0 dropped support for Python 3.6 to 3.9 and deprecated attribute style access on MAPPING values in favour of bracket syntax, and remember that a future 6.0 will drop Python 3.10.
Frequently asked questions
What is the rule-engine library?
It is a Python library providing a lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects. Rules are written as strings, and the language supports regex matching, datetimes, compound datatypes equivalent to the Python dict, list and set types, data attributes, and rule chains that return the value of the first matching rule.
How do I install rule-engine and try the expression language?
Install the latest release from PyPI with `pip install rule-engine`, then run the Debug REPL with `python -m rule_engine.debug_repl` to experiment with the syntax. Releases follow Semantic Versioning so each version indicates whether it fixes bugs, adds features or breaks compatibility.
What does the optional type system in rule-engine do?
You define a context with the symbols and their data types, and then an unknown symbol raises a SymbolResolutionError naming it, while an operation that does not type check raises an EvaluationError. That turns a typo in a user supplied filter into a visible error rather than a rule that silently matches nothing.
Which Python versions does rule-engine support?
Version 5.0 removed support for Python 3.6, 3.7, 3.8 and 3.9, and the current metadata requires Python 3.10 or newer with classifiers through 3.13. Version 6.0 will remove support for Python 3.10, though there is no timeline announced for that release.
What licence is rule-engine released under?
It is released under the BSD 3-Clause licence, with the terms in the LICENSE file at the root of the repository. The documentation states that the library can be used for both commercial and private purposes.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/zerosteiner-rule-engine)Was this useful?