Skip to main content

Evalidate

Evalidate is a simple Python library for safely evaluating untrusted, user-supplied Python expressions. It validates expressions before execution and rejects potentially malicious code.

Evalidate also comes with a convenient, high-performance CLI tool, jg, for filtering JSON lists using Python expressions:

jg 'attributes["storage"] == "512GB" and price<300 and rating>4' products.json

Purpose

Evalidate was originally developed to filter complex data structures. For example, to find inexpensive smartphones that are available for sale:

category=="smartphones" and price<300 and stock>0

However, it can also evaluate other types of expressions, including arithmetic expressions:

a+b-100

Evalidate is the fastest secure Python eval() alternative I know of.

Installation

pip3 install evalidate

TL;DR: Just give me safe eval()!

from evalidate import base_eval_model, EvalException

src = 'a + 10 > b'
# src = "__import__('os').system('clear')"

model = base_eval_model.clone()
expr = model.expr(src)

try:
    print(expr.eval({'a':10, 'b':142}))
except EvalException as e:
    print(e)

Output:

True

If you try to execute the potentially dangerous expression (uncomment the second src line), the output will be:

ERR: Operation type Call is not allowed

Here's a more practical example: finding inexpensive smartphones that meet specific requirements.

from evalidate import base_eval_model, EvalException

mobiles = [
    {'brand': 'Apple',    'model': 'iPhone 15',              'price': 799,  'RAM': 6,  'storage': 128},
    {'brand': 'Apple',    'model': 'iPhone 14',              'price': 799,  'RAM': 6,  'storage': 128},
    {'brand': 'Samsung',  'model': 'Galaxy S23 Ultra',       'price': 1199, 'RAM': 8,  'storage': 256},
    {'brand': 'Samsung',  'model': 'Galaxy A54',             'price': 449,  'RAM': 8,  'storage': 128},
    {'brand': 'Samsung',  'model': 'Galaxy Z Flip 5',        'price': 999,  'RAM': 8,  'storage': 256},
    {'brand': 'Google',   'model': 'Pixel 8',                'price': 699,  'RAM': 8,  'storage': 128},
    {'brand': 'Google',   'model': 'Pixel 7a',               'price': 499,  'RAM': 8,  'storage': 128},
    {'brand': 'OnePlus',  'model': 'OnePlus 11',             'price': 699,  'RAM': 16, 'storage': 256},
    # intentionally incomplete:
    {'brand': 'Xiaomi',   'model': 'Xiaomi 13 Pro',          'price': 999,  'RAM': 12, },
    {'brand': 'Xiaomi',   'model': 'Redmi Note 12 Pro',      'price': 299,  'RAM': 8,  'storage': 256},
]

src = 'storage >= 256 and RAM >= 8 and price <= 1000'

model = base_eval_model.clone()
try:
    expr = model.expr(src)
except EvalException as e:
    print(f"Error in expression ({src}): {e}")
    exit(1)

for mobile in mobiles:
    try:
        if expr.eval(mobile):
            print(f"{mobile['brand']} {mobile['model']} {mobile['storage']}/{mobile['RAM']} ({mobile['price']})")

    except EvalException as e:
        print(f"ERROR: evaluating expression for {mobile['brand']} {mobile['model']}: {e}")

Security

Python's built-in functions, such as compile() and eval(), are powerful enough to execute arbitrary user-supplied code. Using them with untrusted input can therefore be dangerous. For example, an expression might attempt to execute os.system("rm -rf /").

Evalidate uses a whitelist-based approach: an expression is accepted only if it consists of operations explicitly permitted by the configured security model. What counts as safe depends on the model's configuration and the author's security assumptions. Your requirements may differ, so you can define your own list of allowed operations.

Although the built-in models are designed to be secure, Evalidate allows you to extend them. Do so carefully: even an attribute that appears harmless, such as .format, can introduce vulnerabilities.

Exceptions

Evalidate defines three exception classes: CompilationException, ValidationException, and ExecutionException. All three inherit from the base exception class EvalException.

Models: Configuring Validation

Evalidate is highly configurable. Whether an expression passes validation or raises an exception depends on the security model used.

EvalModel defines the security policy for expression evaluation. It specifies the allowed AST nodes, function calls, and attributes, as well as a dictionary of imported functions. Evalidate provides a built-in model, base_eval_model, which permits a basic set of operations considered safe by the author.

You can create an empty model and extend it as needed:

my_model = evalidate.EvalModel()

By default, an empty model allows no operations. Even 1+2 is rejected.

Alternatively, you can clone base_eval_model and extend it:

from evalidate import base_eval_model

my_model = base_eval_model.clone()
my_model.nodes.append('Mult')

expr = my_model.expr('2*2')
expr.eval()

To enable calls to int(), allow the Call node and add int to the list of permitted functions:

my_model.nodes.append('Call')
my_model.allowed_functions.append('int')

my_model.expr('int(36.6)').eval()

To enable attribute access and method calls, configure the model as follows:

m = base_eval_model.clone()
m.nodes.extend(['Call', 'Attribute'])
m.attributes.append('startswith')

src = '"abcdef".startswith("abc")'

r = m.expr(src).eval()
print(r)

Even with this configuration, an expression such as __builtins__["eval"](1) is rejected.

Exporting Functions to Expressions

You can explicitly expose your own functions to evaluated expressions:

from evalidate import base_eval_model

def one():
    return 1

m = base_eval_model.clone()
m.nodes.append('Call')
m.imported_functions["one"] = one
m.expr('one()').eval()

Testing a Model's Security

Evalidate includes a built-in collection of potential attacks and a test_security() method that tests a model against them.

Passing test_security() is necessary, but not sufficient, to establish that a model is secure. Even if test_security() returns True, the model may still be vulnerable to attacks that the test suite does not cover.

from evalidate import base_eval_model

m = base_eval_model.clone()
m.nodes.append('Pow')

assert(m.test_security())

src = '10**10**10'
m.expr(src).eval()

CLI Tools

genfakeproducts

genfakeproducts generates a JSON array containing a specified number of fake product records for testing.

For example, to generate one million records (this takes about one minute on my computer):

genfakeproducts -n 1000000 -o products.json

By default, it generates 10,000 records and writes them to products.json. This takes approximately one second.

Example record:

{
  "id": "d5af9f93-c164-4872-a535-4fb56cf62e6e",
  "sku": "SKU-D5AF9F93",
  "title": "Google ET-3843 Pro",
  "brand": "Google",
  "category": "phones",
  "description": "Foot trial city avoid he wish real college investment tend include draw window either over somebody history detail risk support glass. Google phones designed for modern users.",
  "price": 340.94,
  "currency": "USD",
  "stock": 24,
  "rating": 4.23,
  "reviews_count": 222,
  "attributes": {
    "storage": "512GB",
    "color": "red"
  },
  "tags": [
    "limited",
    "new"
  ]
}

To use genfakeproducts, install Evalidate with the generate extra:

pip3 install evalidate[generate]

jg (JSON grep)

jg is a high-performance CLI tool for filtering JSON data using Python expressions. It accepts an expression and a filename, or reads input from standard input.

jg 'category=="phones"' products.json

jg implements only a small subset of jq's functionality, but it is twice as fast and much simpler to use.

Options

  • -b — Benchmark mode. Suppresses JSON output and measures filtering time.
  • -v — Verbose mode.
  • -l — Read input as JSON Lines (JSONL), with one JSON object per line, instead of a JSON array.
  • -k — Specify the key path to the list, such as "shop::products::onstock", when the input is a nested dictionary.
  • -f — Specify a custom output format for non-JSON output, e.g. '{sku} {price} ({stock}) {title!r}'.

Advanced Topics

Improve Performance by Using Native eval() on Validated Code

Evalidate is fast, but its evaluation machinery still introduces some overhead. If you need maximum performance, you can use Python's native eval() directly on the validated code:

from evalidate import base_eval_model

m = base_eval_model.clone()
d = dict(a=1, b=2)
expr = m.expr('a+b')
r = eval(expr.code, None, d) # <-- native python eval, will run at eval() speed
print(r)

This approach provides the same expression-validation guarantees as Model.Expr.eval(), because expr.code has already passed validation.

The differences are minor but important: executing expr.code directly can raise arbitrary exceptions, whereas expr.eval() raises only ExecutionException. Also, if you want to expose functions to the expression, you must do so manually.

Custom Mapping Classes

You can use custom mapping classes instead of dict. As with native Python eval(), a custom mapping class can be used only as the local evaluation context.

Example:

from collections import UserDict
from evalidate import base_eval_model

class LazyDict(UserDict):
    def __missing__(self, key):
        return 42

ctx = LazyDict(a=100)

model = base_eval_model.clone()

expr = model.expr("a+b")

res = expr.eval(ctx_locals=ctx)
print(res) # 142

Limitations

Evalidate uses ast.parse() to generate an abstract syntax tree (AST), which it then validates.

Warning

A sufficiently large or deeply nested input string can crash the Python interpreter because of stack-depth limitations in Python's AST compiler.

In my tests, expressions containing 200 nested int() calls work:

int(int(.... int(1)...))

but expressions with 201 nested calls do not. The source code is over 1,000 characters long. Even if Evalidate receives such an expression, it will raise CompilationException.

Similar Projects and Benchmarks

asteval

asteval supports more complex code, including function definitions and access to Python's mathematical libraries. However, it has two drawbacks:

  • It is significantly slower than Evalidate, which can evaluate validated code at native eval() speed.
  • User-supplied expressions can run for a long time and consume excessive resources.

simpleeval

simpleeval is a similar project that also uses an AST-based approach and is optimized for repeatedly evaluating pre-parsed expressions.

However, it evaluates expressions using a higher-level representation based on ast.Expr, which makes evaluation several times slower. Evalidate instead compiles validated expressions into Python's native code objects, allowing execution at native eval() speed.

Evalidate is particularly well suited to evaluating the same expression against many different data objects.

Benchmarking

The benchmark script is benchmark/benchmark.py in this repository.

The benchmark creates a list of one million products. In practice, it takes a sample of 100 products and repeats it 10,000 times. It then filters the list using an untrusted user-supplied expression to find matching products. The expression in this test is price < 20.

Products: 1000000 items
evalidate_raw_eval(): 0.266s
evalidate_eval(): 0.326s
test_simpleeval(): 1.824s
test_asteval(): 26.106s

As the results show, Evalidate is several times faster than simpleeval, and both are significantly faster than asteval.

The benchmark may not be optimally implemented; I am not an expert in simpleeval or asteval. If you can suggest a faster filtering implementation, I will consider including it. Any proposed benchmark code must treat the expression as unknown in advance and potentially untrusted.

Further Reading: The Risks of eval()

Note: The Real Python article demonstrates a concise source-validation technique based on code.co_names. However, this technique is vulnerable: it accepts the "bomb" from Ned Batchelder's article, which has an empty co_names tuple and can crash the interpreter.

Evalidate can reject this code and similar bombs, unless you deliberately configure the model to allow the specific operations required by an attack. Evalidate makes it harder to shoot yourself in the foot, but it is still possible if you try hard enough.

More Information

For more information, see the module's source code. It is short, straightforward, and easy to modify.

Contact

Email: yaroslaff at gmail.com

Metadata

Release files for evalidate 2.1.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for evalidate 2.1.6
File Size Uploaded Provenance
evalidate-2.1.6.tar.gz 15.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for evalidate 2.1.6
File Interpreter ABI Platform
evalidate-2.1.6-py3-none-any.whl Python 3 none any Details

Total release size: 29.5 kB

Release files / evalidate-2.1.6.tar.gz

Download URL evalidate-2.1.6.tar.gz
Size 15.4 kB
Tags Source
SHA-256 checksum
How to use checksums
2c1813f44515f97249623b28a639ea890b8a9585c2fa1b695692d38f3fd73f12
BLAKE2b-256 checksum
How to use checksums
73011352ac90c55c1b879315d83ff1e098c53cc5d37cd873a1c390d1add311ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via Hatch/1.16.1 cpython/3.13.5 HTTPX/0.28.1

Release files / evalidate-2.1.6-py3-none-any.whl

Download URL evalidate-2.1.6-py3-none-any.whl
Size 14.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4f77e75172027a78923b3a7e5c92b363047ce044d18a4340dee2fc12e68824dd
BLAKE2b-256 checksum
How to use checksums
21bdb0421d5d4e3e6a75c8e39a84a995cba6907126d270abeb63f4c1e6dfb9de
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via Hatch/1.16.1 cpython/3.13.5 HTTPX/0.28.1

Security for evalidate 2.1.6

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

2.1.6 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page