Data Types
The following table describes the data types supported by the Rule Engine and the Python data types that each is compatible with. For a information regarding supported operations, see the Supported Operations table.
Rule Engine Data Type |
Compatible Python Types |
anything callable |
|
|
|
inner type or
|
|
any (schema-driven) |
|
Compound Types
The compound data types (ARRAY, SET, and MAPPING) are all
capable of containing zero or more values of other data types (though it should be noted that
MAPPING keys must be scalars while the values can be anything). The member types of compound
data types can be defined, but only if the members are all of the same type. For an example, an array containing floats
can be defined, and an mapping with string keys to string values can also be defined, but a mapping with string keys to
values that are either floats, strings or booleans may not be completely defined. For more information, see the
section on Compound Data Types in the Getting Started page.
Compound data types are also iterable, meaning that array comprehension operations can be applied to them. Iteration
operations apply to the members of ARRAY and SET values, and the keys of
MAPPING values. This allows the types to behave in the same was as they do in Python.
OBJECT
Added in version 5.0.0.
The OBJECT type represents a user-defined schema with named, typed attributes. Unlike
MAPPING, which is keyed by arbitrary values, an OBJECT has a fixed set of attributes known at
rule parse time. This enables parse-time validation: accessing an unknown attribute raises an
ObjectAttributeError with a suggestion, and item access (obj["name"]) is rejected
outright.
Defining an Object Type
An OBJECT type is created by calling OBJECT with a name and an attribute schema:
import rule_engine
Hero = rule_engine.DataType.OBJECT('Hero', attributes={
'name': rule_engine.DataType.STRING,
'publisher': rule_engine.DataType.STRING,
'first_appearance': rule_engine.DataType.DATETIME,
})
The name is used for nominal type compatibility: two OBJECT types are compatible only when their names match.
Custom Accessors
By default, attribute values are fetched with getattr(). A custom accessor can be provided to support other
backing stores (dictionaries, database rows, etc.):
# use a dict-backed object instead of an attribute-backed one
Hero = rule_engine.DataType.OBJECT('Hero',
attributes={'name': rule_engine.DataType.STRING},
accessor=lambda obj, name: obj[name]
)
Forward References and Recursion
Use DataType.OBJECT.reference() to create a forward-reference placeholder inside an attribute schema.
Self-references are resolved automatically at construction. For self-references, DataType.OBJECT.self is
a shorthand sentinel that avoids repeating the enclosing schema’s name:
Hero = rule_engine.DataType.OBJECT('Hero', attributes={
'name': rule_engine.DataType.STRING,
'nemesis': rule_engine.DataType.OBJECT.self, # resolved to Hero
})
For mutually-recursive types, place both types in the type_resolver dict and the references will be resolved lazily
at rule parse time:
Person = rule_engine.DataType.OBJECT('Person', attributes={
'name': rule_engine.DataType.STRING,
'employer': rule_engine.DataType.OBJECT.reference('Company'),
})
Company = rule_engine.DataType.OBJECT('Company', attributes={
'name': rule_engine.DataType.STRING,
'ceo': rule_engine.DataType.OBJECT.reference('Person'),
})
context = rule_engine.Context(type_resolver={
'employee': Person,
'Person': Person,
'Company': Company,
})
rule = rule_engine.Rule('employee.employer.ceo.name == "Palpatine"', context=context)
From a Dataclass
When the source data is already modeled as a Python dataclass(), the schema can be derived
directly from the field annotations using DataType.OBJECT.from_dataclass():
import dataclasses
import datetime
import typing
import rule_engine
@dataclasses.dataclass
class Hero:
name: str
publisher: str
first_appearance: datetime.datetime
sidekick: typing.Optional[str] = None
HeroType = rule_engine.DataType.OBJECT.from_dataclass('Hero', Hero)
The derived schema reflects three behaviors automatically:
Optional / nullability: a field annotated as
Optional(orT | None) produces aNULLABLE-wrapped attribute type in the resulting schema; non-Optional fields are stored unwrapped. See the NULLABLE section for how to work with nullable attributes in rules.Nested dataclasses: a field whose annotation is itself a dataclass becomes a nested
OBJECT(recursively). Generic containers (e.g.list[Address],dict[str, Address]) are walked so nested dataclasses insideARRAY,SET, andMAPPINGtypes are also expanded.Self and mutual recursion: a field whose annotation refers back to the enclosing dataclass becomes
DataType.OBJECT.self; cycles between sibling dataclasses produceDataType.OBJECT.reference()placeholders that resolve at rule parse time when both schemas are reachable through theContexttype_resolver.
By default (strict=True) a field whose annotation cannot be mapped to a Rule Engine data type raises
ValueError so the schema mistake is caught early. Pass strict=False to instead map the offending
field to UNDEFINED, leaving it selectable but not type-checked at parse time.
For the common case of building a Context directly from a dataclass, the
type_resolver_from_dataclass() helper produces a resolver whose top-level fields are the
dataclass’s own attributes and whose nested OBJECT types are reachable by name. See
Defining Types From A Dataclass for an end-to-end example.
From a SQLAlchemy Model
Projects that already model their data with SQLAlchemy can derive a schema directly
from a mapped class using DataType.OBJECT.from_sqlalchemy(). SQLAlchemy is an optional dependency; it is
only needed when this entry point is actually invoked. Install it with pip install "sqlalchemy>=2.0".
import datetime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
import rule_engine
class Base(DeclarativeBase):
pass
class Hero(Base):
__tablename__ = 'heroes'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
alias: Mapped[str | None]
first_appearance: Mapped[datetime.datetime]
active: Mapped[bool]
HeroType = rule_engine.DataType.OBJECT.from_sqlalchemy('Hero', Hero)
The walker reads column.type.python_type for each mapped column and threads it through
DataType.from_type(). Column nullability (column.nullable) is copied through to the schema’s
per-attribute nullability map. Enum columns become STRING unless the enum class is an
int subclass (such as IntEnum), in which case they become FLOAT to match the
integer values stored at runtime. ARRAY columns become ARRAY(T) where T is the
mapped element type (ARRAY(UNDEFINED) when the element type is itself unmappable under strict=False).
JSON columns report dict as their python_type and therefore map to
MAPPING(UNDEFINED, UNDEFINED) in both strict and non-strict mode; the nested keys and values remain untyped.
By default (strict=True) a
column whose python_type raises NotImplementedError or resolves to a Python type Rule Engine cannot map
(e.g. UUID) raises ValueError. Pass strict=False to instead map those columns to
UNDEFINED; this is usually the right choice for schemas that include dialect-specific types
whose values can not be statically described.
Relationships expand automatically:
uselistcollections (one-to-many/many-to-many) becomeDataType.ARRAYwrapped around the target class’sOBJECTand are always non-nullable on the parent (an empty list represents “no items”).Scalar relationships (
many-to-one/one-to-one) become a nestedOBJECTwhose nullability is derived from the local foreign-key columns.Self-references and cycles are detected during the walk: a relationship back to the root class becomes
DataType.OBJECT.self; a relationship back to another ancestor on the build stack becomes aDataType.OBJECT.reference()placeholder that resolves at rule parse time via theContexttype_resolver.
For the common case of building a Context directly from a mapped class, the
type_resolver_from_sqlalchemy() helper produces a resolver whose top-level symbols
are the root class’s columns and relationships and whose nested OBJECT schemas are reachable by name. See
Defining Types From A SQLAlchemy Model for an end-to-end example.
Polymorphic / inherited mappings, hybrid properties, and column_property aggregates are out of scope; only
mapped Column entries and mapper.relationships are walked. column_property entries
that surface in mapper.columns are silently skipped since they do not expose the metadata the walker needs.
Restrictions
Item access on an
OBJECTis a parse-time error. Useobj.attributeinstead ofobj["attribute"].Containment checks (
"name" in obj) are rejected at parse time.SET(OBJECT(...))is rejected at construction becauseOBJECTvalues are not guaranteed to be hashable. UseARRAY(OBJECT(...))instead.OBJECTtypes are not inferred byfrom_value(). They must be annotated explicitly via thetype_resolver.
NULLABLE
Added in version 5.0.0.
NULLABLE is a one-argument type constructor that wraps another data type to mark a slot as
permitting NoneType (null) at runtime. NULLABLE(T) is structurally distinct from both T and
NULL and is the single source of truth for nullability in the rule engine’s type system.
NULLABLE is produced automatically wherever the source of a type annotation declares optionality:
DataType.from_type()maps Python’sOptional[T]/T | NonetoNULLABLE(from_type(T)).DataType.OBJECT.from_dataclass()wraps attribute types for dataclass fields typed asOptional[T]and for nested dataclass fields that can holdNone.DataType.OBJECT.from_sqlalchemy()wraps attribute types for columns whosenullableisTrue, and for scalar relationships whose local foreign-key columns are nullable.Compound element types (
ARRAY,SET,MAPPINGvalues) storeNULLABLE(T)directly when the member may beNone.
Semantics are Python-style, not SQL three-valued logic. None flows through expressions as the Python None
value. Parse-time checking is strict: operators that do not meaningfully accept None — arithmetic (+, -,
*, /, …), ordered comparisons (<, <=, >, >=), the regex operators (=~, =~~, !~,
!~~), bitwise and bitwise-shift operators, unary minus, containment (x in container), attribute access
(obj.attr), item access (container[key]), slicing (container[a:b]), and function arguments — reject a
NULLABLE(T) operand at parse time with an EvaluationError (or
FunctionCallError for function arguments) whose message points at the discharge
operators. The rule does not parse; the author must discharge nullability first.
Operators that are meaningful on None stay lenient: equality and logical connectives (==, !=, and,
or, not) always accept NULLABLE operands and return plain BOOLEAN; ternary
expressions (cond ? a : b) propagate NULLABLE to the result if either branch is nullable; and safe-navigation
operators accept NULLABLE targets by design.
The grammar exposes two mechanisms for working with nullable values:
left ?? right(null-coalesce) — dischargesNULLABLE. Evaluates toleftwhen it is notNone, elseright. The result type is the peeled type of the left operand, re-wrapped inNULLABLEonly if the right operand is itself nullable or is thenullliteral.Safe attribute access (
obj&.attr) and safe item access (container&[key]) — accept aNULLABLEtarget without raising but do not discharge it. The overall expression remains nullable, so chainingobj&.inner&.leafyields aNULLABLEvalue that a downstream operator must still discharge.
A few Python-style edge cases worth knowing:
A
NULLABLE(BOOLEAN)value used as a ternary condition (flag ? x : y) is accepted at parse time.Noneis falsy, so a null condition evaluates the false branch at runtime.not NULLABLE(BOOLEAN)is accepted and returns plainBOOLEAN.not Noneevaluates toTrue.NULLABLE(T) in container(nullable member, non-nullable container) is accepted.None in [...]returnsFalse, consistent with Python.non_nullable_value ?? nullgives aNULLABLEresult type. The static analysis conservatively wraps the result whenever the right operand isnull, even if the left can never beNoneat runtime.
The legacy attributes_nullable / value_type_nullable kwargs on OBJECT and compound-type
constructors are still accepted in v5 for backward compatibility but emit a DeprecationWarning and will
be removed in v6.0. Wrap the attribute or element type in NULLABLE directly instead.
FLOAT
See Literal FLOAT Values for syntax.
Starting in v3.0.0, the FLOAT datatype is backed by Python’s Decimal object. This
makes the evaluation of arithmetic more intuitive for the audience of rule authors who are not assumed to be familiar
with the nuances of binary floating point arithmetic. To take an example from the decimal documentation, rule
authors should not have to know that 0.1 + 0.1 + 0.1 - 0.3 != 0.
Internally, Rule Engine conversion values from Python float and int objects to
Decimal using their string representation (as provided by repr()) and not
from_float(). This is to ensure that a Python float value of 0.1 that is
provided by an input will match a Rule Engine literal of 0.1. To explicitly pass a binary floating point value, the
caller must convert it using from_float() themselves. To change the behavior of the floating
point arithmetic, a decimal.Context can be specified by the Context object.
Since Python’s Decimal values are not always equivalent to themselves (e.g.
0.1 != Decimal('0.1')) it’s important to know that Rule Engine will coerce and normalize these values. That means
that while in Python 0.1 in [ Decimal('0.1') ] will evaluate to False, in a rule it will evaluate to True
(e.g. Rule('0.1 in numbers').evaluate({'numbers': [Decimal('0.1')]})). This also affects Python dictionaries that
are converted to Rule Engine MAPPING values. While in Python the value
{0.1: 'a', Decimal('0.1'): 'a'} would have a length of 2 with two unique keys, the same value once converted into a
Rule Engine MAPPING would have a length of 1 with a single unique key. For this reason, developers
using Rule Engine should take care to not use compound data types with a mix of Python float and
Decimal values.
FUNCTION
Version v4.0.0 added the FUNCTION datatype. This can be used to make functions available
to rule authors. Rule Engine contains a few builtin functions that can be used by default.
Additional functions must be defined in Python and can either be added to the evaluated object or by
extending the builtin symbols. It is only possible to call a function from within the
rule text. Functions can not be defined by rule authors as other data types can be.
TIMEDELTA
See Literal TIMEDELTA Values for syntax.
Version v3.5.0 introduced the TIMEDELTA datatype, backed by Python’s
timedelta class. This also comes with the ability to perform arithmetic with both
TIMEDELTA and DATETIME values. This allows you to create rules for things
such as “has it been 30 days since this thing happened?” or “how much time passed between two events?”.
The following mathematical operations are supported:
Adding a timedelta to a datetime (result is a datetime)
Adding a timedelta to another timedelta (result is a timedelta)
Subtracting a timedelta from a datetime (result is a datetime)
Subtracting a datetime from another datetime (result is a timedelta)
Subtracting a timedelta from another timedelta (result is a timedelta)
DataType Utilities
is_type() and is_compatible() are the two classmethods for comparing
data type definitions without a rule expression.
is_type() checks whether a data type belongs to the same family as a given sentinel,
ignoring member types for compound types:
DataType.is_type(dt, DataType.ARRAY) # True for ARRAY(STRING), ARRAY(FLOAT), bare ARRAY, …
DataType.is_type(dt, DataType.MAPPING) # True for any parameterized or bare MAPPING
DataType.is_type(dt, DataType.STRING) # True only for STRING itself (scalars are singletons)
is_compatible() additionally recurses into member types and handles
NULLABLE unwrapping — use it when the full structural type matters.