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 |
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.Contextobject 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...
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, andstringas per CEL spec - Value Types: Support heterogeneous values (mixed types) as allowed by CEL spec
- Runtime Behavior: Maps can contain
dyntypes 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