Written by

Sales Engineer at InterSystems
Article Guillaume Rongier · May 18 8m read

Unifying Embedded Python and the Native API with `iris-embedded-python-wrapper`

 

When developing Python applications with InterSystems IRIS, you can quickly end up with several execution contexts:

  • Python launched directly by IRIS with Embedded Python;
  • a regular python3 process that loads the Embedded Python libraries from a local IRIS installation;
  • an external Python application that connects to IRIS through the official native driver.

These three cases are useful, but they do not behave exactly the same way for imports, system configuration, object APIs, and SQL access. The iris-embedded-python-wrapper project provides a stable Python facade to reduce these differences and keep a single entry point: import iris.

The Problem

In a Python project built around IRIS, the same code may need to run in several environments:

  • in an IRIS terminal with iris python iris, or iris session iris followed by :py;
  • in a local Python script launched with python3;
  • in a remote Python service that connects to an IRIS instance.

Without an abstraction layer, several details often need to be handled separately:

  • the Embedded Python iris module is only available when the IRIS runtime is correctly loaded;
  • the native SDK also exposes an iris package, which can create collisions or ambiguous imports;
  • iris.cls(...) and DB-API connections do not follow exactly the same path in embedded and remote modes;
  • on Linux and macOS, dynamic library paths must be prepared before the Python process starts;
  • boundary values such as SQL NULL and empty strings can be represented differently depending on the backend.

The goal of this wrapper is to let the application declare its execution context clearly while keeping business code close between embedded and native modes.

What the Wrapper Provides

The package provides an import iris facade that keeps the most common entry points available:

  • iris.cls(...) to access IRIS classes;
  • iris.connect(...) to configure or open a connection depending on the context;
  • iris.dbapi for PEP 249-style SQL access;
  • iris.runtime to inspect and explicitly control the active mode.

Supported modes are:

Mode Description Example
embedded-kernel Python is launched by IRIS iris python iris or :py
embedded-local python3 loads the Embedded Python libraries from a local IRIS installation IRISINSTALLDIR, iris.connect(path=...)
native-remote Python connects to a remote IRIS instance official native driver intersystems-irispython
unavailable no IRIS backend is available yet configuration required before use

Installation

The package can be installed with pip:

pip install iris-embedded-python-wrapper

The project depends on the official driver:

intersystems-irispython>=5.0.0

For embedded mode, you also need a local InterSystems IRIS installation. To access embedded mode from an external Python process, the %Service_CallIn service must be enabled.

Example: Using Embedded Python

In an Embedded Python shell launched by IRIS:

iris python iris

or from an IRIS session:

USER>:py

you can import iris as usual:

import iris

print(iris.system.Version.GetVersion())
print(iris.runtime.state)

In this context, iris.runtime.state should report:

'embedded-kernel'

If you want to force the wrapper from a local project directory:

PYTHONPATH=/path/to/iris-embedded-python-wrapper iris python iris

Example: Loading IRIS from a Local python3

The embedded-local mode lets you run a regular Python script while using the Embedded Python libraries from an IRIS installation.

On Linux:

export IRISINSTALLDIR=/opt/iris
export LD_LIBRARY_PATH=$IRISINSTALLDIR/bin:$LD_LIBRARY_PATH
python3 my_script.py

On macOS:

export IRISINSTALLDIR=/opt/iris
export DYLD_LIBRARY_PATH=$IRISINSTALLDIR/bin:$DYLD_LIBRARY_PATH
python3 my_script.py

The wrapper also lets you explicitly declare the IRIS installation path:

import iris

iris.connect(path="/opt/iris")

obj = iris.cls("Ens.StringRequest")._New()
obj.StringValue = "hello from embedded-local"

Important point: on Unix, iris.connect(path=...) can configure Python paths at runtime, but it cannot retroactively fix dynamic library resolution if the Python process has already started without the correct LD_LIBRARY_PATH or DYLD_LIBRARY_PATH.

Example: Keeping iris.cls(...) in Remote Native Mode

With the native SDK, remote code normally uses an explicit IRIS handle:

import iris

conn = iris.connect("localhost", 1972, "USER", "SuperUser", "<password>")
db = iris.createIRIS(conn)

req = db.classMethodValue("Ens.StringRequest", "%New")
db.set(req, "StringValue", "hello")
value = db.get(req, "StringValue")

With the wrapper, you can bind the native connection once and keep a syntax close to Embedded Python:

import iris

conn = iris.connect("localhost", 1972, "USER", "SuperUser", "<password>")
iris.runtime.configure(native_connection=conn)

req = iris.cls("Ens.StringRequest")._New()
req.StringValue = "hello"
value = req.StringValue

The native proxy converts the first _ into %, which lets you write _New() in Python to call %New.

Controlling the Runtime Explicitly

iris.runtime is the wrapper's source of truth for the active context.

import iris

ctx = iris.runtime.get()

print(ctx.mode)
print(ctx.state)
print(ctx.embedded_available)

Useful properties include:

  • iris.runtime.mode: selected policy, for example auto, embedded, or native;
  • iris.runtime.state: detected state, for example embedded-kernel, embedded-local, native-remote, or unavailable;
  • iris.runtime.embedded_available: whether the embedded backend is usable;
  • iris.runtime.iris: native IRIS handle bound to the runtime, if available;
  • iris.runtime.dbapi: explicitly bound DB-API connection, if available.

You can also force a mode:

import iris

iris.runtime.configure(mode="embedded")

or reset to automatic detection:

iris.runtime.reset()

SQL Access with iris.dbapi

The wrapper exposes a DB-API facade compatible with common usage:

  • connect();
  • cursor();
  • execute();
  • fetchone(), fetchmany(), fetchall();
  • commit(), rollback(), close();
  • PEP 249 exceptions such as InterfaceError, OperationalError, etc.

In embedded mode:

import iris

conn = iris.dbapi.connect(mode="embedded")
cur = conn.cursor()

cur.execute("SELECT Name FROM Sample.Person")
rows = cur.fetchall()

cur.close()
conn.close()

In embedded-local mode with an explicit path:

import iris

conn = iris.dbapi.connect(path="/opt/iris", namespace="USER")
cur = conn.cursor()
cur.execute("SELECT 1")
print(cur.fetchone())

In remote native mode:

import iris

conn = iris.dbapi.connect(
    mode="native",
    hostname="localhost",
    port=1972,
    namespace="USER",
    username="SuperUser",
    password="<password>",
)

cur = conn.cursor()
cur.execute("SELECT 1")
print(cur.fetchone())

In auto mode, the wrapper chooses the backend based on the provided arguments and the state of iris.runtime. For example, if you provide hostname, port, namespace, username, and password, the connection is routed to the native driver.

The wrapper also normalizes some embedded cases to bring behavior closer to the native DB-API:

  • SQL NULL becomes Python None;
  • an empty SQL string becomes "";
  • None passed as a parameter remains SQL NULL;
  • "" passed as a parameter remains an empty SQL string.

Binding a Virtual Environment to Embedded Python

The project provides two practical commands:

bind_iris
unbind_iris

bind_iris looks for the Python library of the current virtual environment, updates the IRIS Embedded Python configuration, and creates a backup of the iris.cpf file.

Example:

python3 -m venv .venv
. .venv/bin/activate
pip install iris-embedded-python-wrapper
bind_iris

Depending on the platform and IRIS configuration, IRIS administrator privileges may be required. On Windows, restarting the IRIS instance may be required after changing the configuration.

When Should You Use This Wrapper?

This wrapper is useful if you want to:

  • write Python IRIS code that can run in embedded or remote mode;
  • reduce differences between Embedded Python and the Native API;
  • make the execution mode observable through iris.runtime;
  • use a single DB-API facade in application code;
  • simplify virtual environment configuration for Embedded Python;
  • test embedded and remote paths more easily.

It is not a replacement for InterSystems IRIS or the official native SDK. It is a convenience layer that builds on top of those components to make application code more portable.

Resources

Conclusion

iris-embedded-python-wrapper provides a simple facade around the main Python execution modes of InterSystems IRIS. The main benefit is being able to write:

import iris

and then explicitly decide whether the code runs in Embedded Python, embedded-local mode, or through a remote native connection. For applications that need to move from one mode to another, or for teams that want to share more code between local scripts, embedded jobs, and external Python services, this approach removes a lot of technical noise.