`iris-persistence`: a Python-first persistence layer for InterSystems IRIS
With Embedded Python and the Native API, it is becoming increasingly natural to write part of IRIS application logic in Python. But one question quickly comes up: how can you manipulate IRIS persistent objects from Python without losing the connection to the native object model, class dictionary, indexes, storage, and SQL projections?

iris-persistence explores that question. The project provides a Python object persistence layer for InterSystems IRIS, inspired by %Persistent. The idea is simple: declare typed Python models, then synchronize them with IRIS classes, or connect to existing IRIS classes without modifying them.
One Python model, one IRIS class
Here is a minimal example:
from typing import Annotated
from iris_persistence import Field, Model
class Product(Model, persistent=True):
name: str = Field(required=True, max_length=200, unique=True)
price: Annotated[float, Field(default=0.0)]
in_stock: bool = True
class Meta:
classname = "Demo.Product"
mode = "replace"
Product.sync_schema()
product = Product(name="Widget", price=12.5, in_stock=True)
product.save()
same = Product.get(product.pk)
rows = Product.where(name="Widget").order_by("name").all()
The Python model declares fields, their types, and part of the IRIS metadata. During sync_schema(), iris-persistence writes into the IRIS dictionary, including class, property, index, and storage definitions. IRIS then compiles the class and remains the source of truth for SQL projection and object runtime behavior.
In this example, unique=True declares uniqueness directly on the Python field. The goal is still to work with IRIS objects, not to handcraft SQL DDL.
Three modes depending on the level of control you want
An important concept in the project is synchronization mode. Not every use case has the same relationship to schema control.
| Mode | Idea | Use case |
|---|---|---|
extend |
Python adds or updates what it declares, without removing the rest | Start cautiously with an existing class |
replace |
Python rebuilds the IRIS class from the model | New schema fully managed from Python |
observe |
Python never modifies schema | Read and manipulate existing IRIS classes |
This split is useful for brownfield projects. You can start in observe mode to bind to existing classes, move to extend to add controlled elements from Python, or use replace for new classes whose schema is fully managed by Python code.
Not just simple fields
The project supports traditional declarations with Field(...), and also typing.Annotated. It can map common Python types to IRIS types:
str,int,float,boolbytesdictandlistdatetime.date,datetime.time,datetime.datetime- references to other
Modelmodels
You can also force the underlying IRIS type when automatic mapping is not enough:
class Event(Model, persistent=True):
payload: bytes = Field(iris_type="%Stream.GlobalBinary")
created_at: str = Field(iris_type="%Library.TimeStamp")
Linked objects and %SerialObject
iris-persistence also handles relationships between persistent models and serialized objects. For example:
from iris_persistence import Field, Model
class Customer(Model, persistent=True):
Name: str = Field(required=True, max_length=120)
class Meta:
classname = "Demo.Customer"
mode = "replace"
class Address(Model, serial=True):
Street: str = Field(required=True, max_length=120)
City: str = Field(required=True, max_length=80)
class Meta:
classname = "Demo.Address"
mode = "replace"
class Order(Model, persistent=True):
OrderNumber: str = Field(required=True, max_length=40, unique=True)
Customer: Customer | None = None
ShipTo: Address | None = None
class Meta:
classname = "Demo.Order"
mode = "replace"
During save operations, referenced objects are materialized in IRIS. A %Persistent can reference another %Persistent, and a %Persistent can embed a %SerialObject.
The repository also contains examples with list and array collections of serialized models.
From an IRIS .cls class to a Python model
An important use case is generating Python models from IRIS classes that already exist in a namespace. Imagine an ObjectScript class compiled in IRIS:
Class Demo.Article Extends %Persistent
{
Property Title As %String(MAXLEN = 200) [ Required ];
Property Body As %String(MAXLEN = 4000);
Property PublishedAt As %TimeStamp;
}
Once this class is available in the active namespace, you can ask iris-persistence to read the IRIS dictionary and generate the Python facade:
from iris_persistence import scaffold_from_iris
generated_files = scaffold_from_iris(
"Demo.Article",
"./generated_models",
mode="observe",
extract_meta=True,
)
The generated file looks like this:
import datetime
from typing import Annotated
from iris_persistence import Field, Model
class Article(Model, persistent=True):
Body: Annotated[str, Field(max_length=4000)]
PublishedAt: datetime.datetime | None = None
Title: Annotated[str, Field(required=True, max_length=200)]
class Meta:
classname = "Demo.Article"
mode = "observe"
observe mode is important here: the Python model can manipulate data, but it never modifies the IRIS class. This is a practical way to progressively expose an existing %Persistent model to typed Python code.
If several classes are linked together, include_related=True can also generate neighboring models when they are referenced by properties of the main class.
Storage and advanced metadata
For more advanced cases, the project exposes metadata dataclasses:
from iris_persistence import (
ClassMetadata,
Field,
Model,
StorageData,
StorageDefinition,
)
class ShowcaseRecord(Model, persistent=True):
Title: str = Field(required=True, max_length=350)
class Meta:
classname = "Demo.ShowcaseRecord"
mode = "replace"
parameters = {"DEFAULTGLOBAL": "^Demo.ShowcaseRecordD"}
metadata = ClassMetadata(
description="advanced schema example",
final=True,
sql_table_name="Demo_ShowcaseRecord",
procedure_block=True,
)
storage = StorageDefinition(
data_location="^Demo.ShowcaseRecordD",
default_data="ShowcaseRecordDefaultData",
type="%Storage.Persistent",
data=(
StorageData(
name="ShowcaseRecordDefaultData",
structure="listnode",
values={
"1": "%%CLASSNAME",
"2": "Title",
},
),
),
)
This level of detail matters: the goal is not only to provide Python CRUD, but to stay close to the real IRIS representation.
Runtime: Embedded Python or Native API
The project uses iris-embedded-python-wrapper as a runtime facade. Two main scenarios are supported.
In Embedded Python, run directly inside IRIS:
import iris_persistence
iris_persistence.configure()
From an external Python process, through the Native API:
import iris
import iris_persistence
conn = iris.connect(host, port, namespace, user, password)
iris_persistence.configure(conn)
The same business logic can therefore be tested and executed in multiple contexts.
What this project is not
iris-persistence is not a generic SQL ORM. Simple queries do use an SQL projection to retrieve IDs, but the core mental model remains IRIS: classes, objects, dictionary, compilation, and storage.
It is also not an abstraction that completely hides IRIS. On the contrary, the API intentionally exposes concepts such as classname, %Persistent, %SerialObject, StorageDefinition, and class parameters.
Conclusion
This project explores a Python-first approach to persistence on InterSystems IRIS, without abandoning the native mechanisms that make IRIS strong. It can be useful to quickly prototype new models in Python, expose existing IRIS classes to typed Python code, or test business behaviors without always starting an IRIS instance.
Community feedback would be especially valuable on three points:
- real brownfield scenarios around existing
%Persistentclasses; - the level of IRIS metadata to expose in the Python API;
- the balance between a simple Python API and fidelity to the IRIS object model.
Comments
This is a really cool project and a great addition to the IRIS Python ecosystem!