Skip to content

Python API Reference

Complete autogenerated reference for the Python CEL library.

Functions

cel.evaluate(src, evaluation_context=None) builtin

Evaluate a Common Expression Language (CEL) expression.

This is the main entry point for the CEL library. It parses, compiles, and evaluates a CEL expression within an optional context, returning the result as a native Python type.

CEL expressions support a wide range of operations including arithmetic, logical operations, string manipulation, list/map operations, and custom function calls. For detailed language reference, see the CEL specification documentation.

Parameters:

Name Type Description Default
src str

The CEL expression to evaluate. Must be a valid CEL expression according to the CEL language specification.

required
evaluation_context Optional[Union[Context, dict]]

An optional context for the evaluation. This can be either: - A cel.Context object (recommended for reusable contexts) - A standard Python dictionary containing variables and functions - None (for expressions that don't require external variables)

None

Returns:

Type Description
Any

Union[bool, int, float, str, list, dict, datetime.datetime, bytes, None]: The result of the expression, automatically converted to the appropriate Python type. Common return types include: - bool: For logical expressions (e.g., "1 < 2") - int/float: For arithmetic expressions - str: For string operations - list: For list expressions and operations - dict: For map/object expressions - datetime.datetime: For timestamp operations - bytes: For byte array operations - None: For null values

Raises:

Type Description
ValueError

If the expression has a syntax error, fails to parse, or is malformed. This includes issues such as: - Unclosed quotes or parentheses - Invalid CEL syntax - Empty expressions

TypeError

If an operation is attempted on incompatible types, such as: - Adding incompatible types (e.g., string + int without conversion) - Mixing signed and unsigned integers in arithmetic - Using unsupported operators between specific types

RuntimeError

For evaluation errors that occur during execution: - Referencing undefined variables or functions - Errors from custom Python functions - Internal evaluation failures

Performance Notes
  • For multiple evaluations with the same context, use a cel.Context object for better performance and memory efficiency.
  • Complex expressions are compiled once and can be cached internally.

Examples:

Basic arithmetic and logical operations:

>>> from cel import evaluate
>>> evaluate("1 + 2 * 3")
7
>>> evaluate("'Hello' + ' ' + 'World'")
'Hello World'
>>> evaluate("[1, 2, 3].size() > 2")
True

Using variables from a dictionary context:

>>> user_data = {"name": "Alice", "age": 30, "roles": ["admin", "user"]}
>>> evaluate("name + ' is ' + string(age) + ' years old'", user_data)
'Alice is 30 years old'
>>> evaluate("'admin' in roles", user_data)
True

Working with nested data structures:

>>> context = {
...     "user": {"profile": {"name": "Bob", "verified": True}},
...     "settings": {"theme": "dark", "notifications": False}
... }
>>> evaluate("user.profile.verified && settings.theme == 'dark'", context)
True

Using custom Python functions:

>>> def calculate_discount(price, percentage):
...     return price * (1 - percentage / 100)
>>> context = {
...     "price": 100.0,
...     "discount_rate": 15,
...     "calculate_discount": calculate_discount
... }
>>> evaluate("calculate_discount(price, discount_rate)", context)
85.0

Error handling example:

>>> try:
...     evaluate("undefined_variable + 5")
... except RuntimeError as e:
...     print(f"Error: {e}")
Error: Undefined variable or function: 'undefined_variable'...

Using Context object for reusable evaluations:

>>> from cel import Context
>>> context = Context(
...     variables={"base_url": "https://api.example.com"},
...     functions={"len": len}
... )
>>> evaluate("base_url + '/users'", context)
'https://api.example.com/users'
>>> evaluate("len('hello world')", context)
11

Type safety and error handling:

>>> # Strict CEL mode enforces type compatibility
>>> evaluate("1.0 + 2.5")  # Same type - works
3.5
>>> try:
...     evaluate("1 + 2.5")  # Mixed types - fails
... except TypeError as e:
...     print("Type error:", e)
Type error: Unsupported addition operation: Int + Double...
>>> # Use explicit conversion for mixed arithmetic
>>> evaluate("double(1) + 2.5")
3.5
See Also
  • cel.Context: For managing reusable evaluation contexts
  • CEL Language Guide: For comprehensive language documentation
  • Python API Reference: For detailed API documentation

compile(expression: str) -> Program

Compile a CEL expression into a reusable Program object.

This function parses and compiles a CEL expression, returning a Program object that can be executed multiple times with different contexts. This is more efficient than calling evaluate() repeatedly with the same expression.

Parameters: - expression: The CEL expression to compile

Returns: - A compiled Program object

Raises: - ValueError: If the expression has syntax errors or is malformed

Example:

import cel

# Compile once
program = cel.compile("x + y")

# Execute many times with different contexts
result1 = program.execute({"x": 1, "y": 2})
assert result1 == 3  # → 3

result2 = program.execute({"x": 10, "y": 20})
assert result2 == 30  # → 30

When to use compile() vs evaluate():

  • Use evaluate() for one-time evaluation or interactive/REPL usage
  • Use compile() + execute() when evaluating the same expression with many different contexts or in performance-critical loops

Classes

Program

A compiled CEL program that can be executed multiple times with different contexts.

The Program class represents a pre-compiled CEL expression. Use this when you need to evaluate the same expression many times with different variable bindings. Compiling once and executing multiple times is significantly faster than calling evaluate() repeatedly.

import cel

# Compile the expression once
program = cel.compile("price * quantity > 100")

# Execute many times with different contexts
result1 = program.execute({"price": 10, "quantity": 20})
assert result1 == True  # → True (200 > 100)

result2 = program.execute({"price": 5, "quantity": 10})
assert result2 == False  # → False (50 > 100)

Methods

execute(context=None) -> Any

Execute the compiled program with the given context.

Parameters: - context: Optional evaluation context (dict or Context object)

Returns: - The result of the expression evaluation

Raises: - RuntimeError: If a variable or function is undefined - TypeError: If there's a type mismatch during execution - ValueError: If the context is an invalid type

Example with dict context:

import cel

program = cel.compile("user.name + ' is ' + user.role")
result = program.execute({
    "user": {"name": "Alice", "role": "admin"}
})
assert result == "Alice is admin"

Example with Context object:

import cel
from cel import Context

program = cel.compile("greet(name)")

ctx = Context()
ctx.add_variable("name", "World")
ctx.add_function("greet", lambda x: f"Hello, {x}!")

result = program.execute(ctx)
assert result == "Hello, World!"

Performance pattern - compile once, execute many:

import cel

# Access control policy - compiled once at startup
policy = cel.compile(
    'user.role == "admin" || resource.owner == user.id'
)

# Evaluated many times per request
def check_access(user, resource):
    return policy.execute({"user": user, "resource": resource})

# Fast repeated evaluation
assert check_access({"id": "alice", "role": "admin"}, {"owner": "bob"}) == True
assert check_access({"id": "bob", "role": "user"}, {"owner": "bob"}) == True
assert check_access({"id": "charlie", "role": "user"}, {"owner": "bob"}) == False

OptionalValue

Wrapper for CEL optional values.

CEL optional values preserve the distinction between "no value" and "a value that is null". The Python wrapper keeps that distinction intact.

import cel

opt = cel.evaluate("optional.of(42)")
assert isinstance(opt, cel.OptionalValue)
assert opt.has_value() is True
assert opt.value() == 42
assert opt.or_value(0) == 42

none_opt = cel.evaluate("optional.none()")
assert none_opt.has_value() is False
assert none_opt.or_value("default") == "default"

Distinguishing optional.none() from optional.of(null):

import cel

opt_null = cel.evaluate("optional.of(null)")
assert opt_null.has_value() is True
assert opt_null.value() is None

opt_none = cel.evaluate("optional.none()")
assert opt_none.has_value() is False

Passing OptionalValue into evaluation contexts:

import cel

opt = cel.OptionalValue.of(123)
assert cel.evaluate("opt.orValue(0)", {"opt": opt}) == 123

opt_none = cel.OptionalValue.none()
assert cel.evaluate("opt.orValue(7)", {"opt": opt_none}) == 7

Methods

of(value) -> OptionalValue

Create an optional value containing value.

none() -> OptionalValue

Create an empty optional value.

has_value() -> bool

Return True when the optional contains a value.

value() -> Any

Return the contained value or raise ValueError for optional.none().

or_value(default) -> Any

Return the contained value if present, otherwise default.

or_optional(other) -> OptionalValue

Return self if it has a value, otherwise return other.


Context

A class for managing evaluation context with variables and custom functions.

The Context class provides more control over the evaluation environment than simple dictionary context. It allows you to:

  • Add variables with type checking
  • Register custom Python functions
  • Manage complex evaluation scenarios
from cel import evaluate, Context

# Basic usage
context = Context()
context.add_variable("name", "Alice")
context.add_variable("age", 30)

result = evaluate("name + ' is ' + string(age)", context)
# → "Alice is 30"
assert result == "Alice is 30"

Methods

add_variable(name: str, value: Any) -> None

Add a variable to the context.

Parameters: - name: Variable name (must be valid CEL identifier) - value: Variable value (will be converted to appropriate CEL type)

Example:

from cel import Context, evaluate

context = Context()
context.add_variable("user_id", "123")
context.add_variable("permissions", ["read", "write"])
context.add_variable("config", {"debug": True, "port": 8080})

# Verify the variables are accessible
evaluate("user_id", context)
# → "123"
evaluate("size(permissions)", context)
# → 2
evaluate("config.debug", context)
# → True
assert evaluate("user_id", context) == "123"
assert evaluate("size(permissions)", context) == 2
assert evaluate("config.debug", context) == True

update(variables: Dict[str, Any]) -> None

Add multiple variables at once.

Parameters: - variables: Dictionary of variable names to values

Example:

from cel import Context, evaluate

context = Context()
context.update({
    "user_id": "123",
    "role": "admin", 
    "permissions": ["read", "write", "delete"]
})

# Verify the batch update worked
evaluate("user_id", context)
# → "123"
evaluate("role", context)
# → "admin"
evaluate("size(permissions)", context)
# → 3
assert evaluate("user_id", context) == "123"
assert evaluate("role", context) == "admin"
assert evaluate("size(permissions)", context) == 3

add_function(name: str, func: Callable) -> None

Register a Python function for use in CEL expressions.

Parameters: - name: Function name as it will appear in CEL expressions - func: Python function to register

Requirements for functions: - Should handle type conversions appropriately - Should raise meaningful exceptions for invalid inputs - Must be callable from the Python environment

Example:

from cel import Context, evaluate

def validate_email(email):
    import re
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return re.match(pattern, email) is not None

context = Context()
context.add_function("validate_email", validate_email)

evaluate('validate_email("user@example.com")', context)
# → True

# Test invalid email
evaluate('validate_email("invalid-email")', context)
# → False
result = evaluate('validate_email("user@example.com")', context)
assert result == True

result = evaluate('validate_email("invalid-email")', context)
assert result == False

set_variable_resolver(resolver: Callable[[str], Any]) -> None

Register a callback for lazy variable resolution. Instead of materializing every variable upfront with add_variable, the resolver is invoked on demand with the name of each variable an expression references.

Parameters: - resolver: A callable taking a variable name (str) and returning either the value or None. Returning None falls through to variables registered via add_variable.

When to use it: - The full variable set is expensive to build (database queries, file I/O, remote API calls) and you only want to pay for variables the expression actually references. - The variable set isn't known ahead of time — the resolver decides on the fly.

Behaviour notes: - The resolver is consulted before statically-registered variables. Return None from the resolver to delegate to those. - Exceptions raised by the resolver are logged and treated as None — they do not propagate to the caller. - The callback runs synchronously while the GIL is held; keep it tight.

Example — only load what's referenced:

import json
import os
import tempfile
from cel import Context, evaluate

with tempfile.TemporaryDirectory() as cfg_dir:
    # Each "setting" is a file on disk
    for name, content in [("max_users", "100"), ("admin_email", '"ops@example.com"')]:
        with open(os.path.join(cfg_dir, name), "w") as f:
            f.write(content)

    loaded = []

    def load_setting(name):
        path = os.path.join(cfg_dir, name)
        if not os.path.exists(path):
            return None
        loaded.append(name)
        with open(path) as f:
            return json.loads(f.read())

    ctx = Context()
    ctx.set_variable_resolver(load_setting)

    # Only `max_users` is referenced — `admin_email` is never loaded
    assert evaluate("max_users > 50", ctx) is True
    assert loaded == ["max_users"]

Example — combine with statically-registered variables:

from cel import Context, evaluate

# Resolver handles dynamic lookups; static variables provide defaults
ctx = Context(variables={"environment": "prod"})
ctx.set_variable_resolver(lambda name: {"feature_flags": ["new_ui"]}.get(name))

assert evaluate("environment", ctx) == "prod"          # → static
assert evaluate("'new_ui' in feature_flags", ctx) is True  # → resolver

Type System

CEL to Python Type Mapping

This table shows how CEL types are converted to Python types when expressions are evaluated:

CEL Type CEL Spec Python Type Example CEL Python Result
int 64-bit signed integers int 42 42
uint 64-bit unsigned integers int 42u 42
double 64-bit IEEE floating-point float 3.14 3.14
bool Boolean values bool true True
string Unicode code point sequences str "hello" "hello"
bytes Byte sequences bytes b"data" b"data"
null_type Null value NoneType null None
list Ordered sequences list [1, 2, 3] [1, 2, 3]
map Key-value collections dict {"key": "value"} {"key": "value"}
timestamp Protocol buffer timestamps datetime.datetime timestamp("2024-01-01T00:00:00Z") datetime(2024, 1, 1, tzinfo=timezone.utc)
duration Protocol buffer durations datetime.timedelta duration("1h30m") timedelta(hours=1, minutes=30)

Map Type Constraints

✅ FULLY COMPLIANT with CEL specification:

  • Key Types: Restricted to int, uint, bool, and string as per CEL spec
  • Value Types: Support heterogeneous values (mixed types) as allowed by CEL spec
  • Runtime Behavior: Maps can contain dyn types for mixed-value collections

Examples:

// ✅ Valid key types
{1: "int key", "str": "string key", true: "bool key"}

// ✅ Mixed value types (CEL compliant)
{"name": "Alice", "age": 30, "verified": true, "score": 95.5}

// ✅ Nested heterogeneous structures  
{"users": [{"name": "Alice"}, {"name": "Bob"}], "count": 2}

Python to CEL Type Mapping

When passing Python objects as context:

Python Type CEL Type Notes
int int Direct mapping
float double Direct mapping
str string Direct mapping
bool bool Direct mapping
None null Direct mapping
list list(T) Element types preserved
dict map(K, V) Key/value types preserved
bytes bytes Direct mapping
datetime.datetime timestamp Timezone info preserved
datetime.timedelta duration Direct mapping

Error Handling

Exception Types

The library raises specific exception types for different error conditions based on the underlying error type:

ValueError - Parse and Compilation Errors

Raised when the CEL expression has invalid syntax, is empty, or fails to compile:

from cel import evaluate

# Invalid syntax raises ValueError
try:
    evaluate("1 + + 2")  # Invalid syntax
    # → ValueError: Failed to parse expression: ...
    assert False, "Should have raised ValueError"
except ValueError as e:
    assert "Failed to parse expression" in str(e)

# Empty expression raises ValueError
try:
    evaluate("")
    # → ValueError: Failed to parse expression
    assert False, "Should have raised ValueError"
except ValueError as e:
    assert "Failed to parse expression" in str(e)

RuntimeError - Variable and Function Errors

Raised for undefined variables or functions, and function execution errors:

from cel import evaluate

# Undefined variables raise RuntimeError
try:
    evaluate("unknown_variable + 1", {})
    # → RuntimeError: Undefined variable 'unknown_variable'
    assert False, "Should have raised RuntimeError"
except RuntimeError as e:
    assert "Undefined variable" in str(e)

# Undefined functions raise RuntimeError
try:
    evaluate("unknownFunction(42)", {})
    # → RuntimeError: Undefined function 'unknownFunction'
    assert False, "Should have raised RuntimeError"
except RuntimeError as e:
    assert "Undefined" in str(e) and "function" in str(e)

# Function execution errors raise RuntimeError
from cel import Context
def failing_function():
    raise Exception("Something went wrong")

context = Context()
context.add_function("fail", failing_function)

try:
    evaluate("fail()", context)
    # → RuntimeError: Function 'fail' error: Something went wrong
    assert False, "Should have raised RuntimeError"
except RuntimeError as e:
    assert "Function 'fail' error" in str(e)

TypeError - Type Compatibility Errors

Raised when operations are performed on incompatible types:

from cel import evaluate

# String + int operations raise TypeError
try:
    evaluate('"hello" + 42')  # String + int
    # → TypeError: No such overload (or Unsupported addition operation, depending on operand order)
    assert False, "Should have raised TypeError"
except TypeError as e:
    assert "overload" in str(e).lower() or "Unsupported addition operation" in str(e)

# Mixed signed/unsigned int operations raise TypeError
try:
    evaluate("1u + 2")  # Mixed signed/unsigned int
    # → TypeError: Cannot mix signed and unsigned integers (or "No such overload" depending on order)
    assert False, "Should have raised TypeError"
except TypeError as e:
    assert "overload" in str(e).lower() or "signed and unsigned" in str(e)

# Unsupported multiplication raises TypeError
try:
    evaluate('"text" * "more"')  # String multiplication
    # → TypeError: No such overload (or Unsupported multiplication operation)
    assert False, "Should have raised TypeError"
except TypeError as e:
    assert "overload" in str(e).lower() or "Unsupported multiplication operation" in str(e)

Mixed Type Arithmetic Errors

Mixed numeric types raise TypeError:

from cel import evaluate

# Mixed numeric types in expressions
try:
    evaluate("1 + 2.5")  # int + double
except TypeError as e:
    assert "overload" in str(e).lower() or "Unsupported addition operation" in str(e)
    print(f"Mixed arithmetic error: {e}")

# Mixed types from context
context = {"int_val": 10, "float_val": 2.5}
try:
    evaluate("int_val * float_val", context)
except TypeError as e:
    assert "overload" in str(e).lower() or "Unsupported multiplication operation" in str(e)
    print(f"Context type mixing error: {e}")

# To fix mixed arithmetic, use consistent types:
result = evaluate("1.0 + 2.5")  # → 3.5 (both doubles)
result = evaluate("1 + 2")      # → 3 (both ints)

Production Error Handling

For comprehensive error handling patterns, safety guidelines, and production best practices, see the Error Handling How-To Guide which covers:

  • Safe handling of malformed expressions and untrusted input
  • Safe evaluation wrappers and best practices
  • Context validation patterns
  • Defensive expression techniques
  • Logging and monitoring
  • Testing error scenarios