Skip to main content

Bridging the Gap Between ObjectScript and Embedded Python

Because of the differences between the ObjectScript and Python languages, you will need to know a few pieces of information that will help you bridge the gap between the languages.

From the ObjectScript side, the %SYS.PythonOpens in a new tab class allows you to use Python from ObjectScript. See the InterSystems IRIS class reference for more information.

From the Python side, the iris module allows you to use ObjectScript from Python. From Python, type help(iris) for a list of its methods and functions.

Use Python Builtin Functions

The builtins package is loaded automatically when the Python interpreter starts, and it contains all of the language’s built-in identifiers, such as the base object class and all of the built-in datatype classes, exceptions classes, functions, and constants.

You can import this package into ObjectScript to gain access to all of these identifiers as follows:

set builtins = ##class(%SYS.Python).Import("builtins")

The Python print() function is actually a method of the builtins module, so you can now use this function from ObjectScript:

USER>do builtins.print("hello world!")
hello world!

You can then use the zwrite command to examine the builtins object, and since it is a Python object, it uses the str() method of the builtins package to get a string representation of that object. For example:

USER>zwrite builtins
builtins=5@%SYS.Python  ; <module 'builtins' (built-in)>  ; <OREF>

By the same token, you can create a Python list using the method builtins.list(). The example below creates an empty list:

USER>set list = builtins.list()
USER>zwrite list
list=5@%SYS.Python  ; []  ; <OREF>

You can use the builtins.type() method to see what Python type the variable list is:

USER>zwrite builtins.type(list)
3@%SYS.Python  ; <class 'list'>  ; <OREF>

Interestingly, the list() method actually returns an instance of Python’s class object that represents a list. You can see what methods the list class has by using the dir() method on the list object:

USER>zwrite builtins.dir(list)
3@%SYS.Python  ; ['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', 
'__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', 
'__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__',  
'__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__','__repr__', '__reversed__',  
'__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append','clear', 
'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']  ; <OREF>

Likewise, you can use the help() method to get help on the list object.

Help on list object:
class list(object)
 |  list(iterable=(), /)
 |  Built-in mutable sequence.
 |  If no argument is given, the constructor creates a new empty list.
 |  The argument must be an iterable if specified.
 |  Methods defined here:
 |  __add__(self, value, /)
 |      Return self+value.
 |  __contains__(self, key, /)
 |      Return key in self.
 |  __delitem__(self, key, /)
 |      Delete self[key].

Instead of importing the builtins module into ObjectScript, you can call the Builtins() method of the %SYS.PythonOpens in a new tab class.

Identifier Names

The rules for naming identifiers are different between ObjectScript and Python. For example, the underscore (_) is allowed in Python method names, and in fact is widely used for the so-called “dunder” methods and attributes (“dunder” is short for “double underscore”), such as __getitem__ or __class__. To use such identifiers from ObjectScript, enclose them in double quotes:

USER>set mylist = builtins.list()
USER>zwrite mylist."__class__"
2@%SYS.Python  ; <class list>  ; <OREF>

Conversely, InterSystems IRIS methods often begin with a percent sign (%). such as %New() or %Save(). To use such identifiers from Python, replace the percent sign with an underscore. If you have a persistent class User.Person, the following line of Python code creates a new Person object.

>>> import iris
>>> p = iris.cls('User.Person')._New()

Keyword or Named Arguments

A common practice in Python is to use keyword arguments (also called “named arguments”) when defining a method. This makes it easy to drop arguments when not needed or to specify arguments according to their names, not their positions. As an example, take the following simple Python method:

def mymethod(foo=1, bar=2, baz="three"):
    print(f"foo={foo}, bar={bar}, baz={baz}")

Since InterSystems IRIS does not have the concept of keyword arguments, you need to create a dynamic object to hold the keyword/value pairs, for example:

set args={ "bar": 123, "foo": "foo"}

If the method mymethod() were in a module called, you could import it into ObjectScript and then call it, as follows:

USER>set obj = ##class(%SYS.Python).Import("mymodule")
USER>set args={ "bar": 123, "foo": "foo"}
USER>do obj.mymethod(args...)
foo=foo, bar=123, baz=three

Since baz was not passed in to the method, it is assigned the value of "three" by default.

Passing Arguments By Reference

Arguments in methods written in ObjectScript can be passed by value or by reference. In the method below, the ByRef keyword in front of the second and third arguments in the signature indicates that they are intended to be passed by reference.

ClassMethod SandwichSwitch(bread As %String, ByRef filling1 As %String, ByRef filling2 As %String)
    set bread = "whole wheat"
    set filling1 = "almond butter"
    set filling2 = "cherry preserves"

When calling the method from ObjectScript, place a period before an argument to pass it by reference, as shown below:

USER>set arg1 = "white bread"
USER>set arg2 = "peanut butter"
USER>set arg3 = "grape jelly"
USER>do ##class(User.EmbeddedPython).SandwichSwitch(arg1, .arg2, .arg3)
USER>write arg1
white bread
USER>write arg2
almond butter
USER>write arg3
cherry preserves

From the output, you can see that the value of the variable arg1 remains the same after calling SandwichSwitch(), while the values of the variables arg2 and arg3 have changed.

Since Python does not support call by reference natively, you need to use the iris.ref() method to create a reference to pass to the method for each argument to be passed by reference:

>>> import iris
>>> arg1 = "white bread"
>>> arg2 = iris.ref("peanut butter")
>>> arg3 = iris.ref("grape jelly")
>>> iris.cls('User.EmbeddedPython').SandwichSwitch(arg1, arg2, arg3)
>>> arg1
'white bread'
>>> arg2.value
'almond butter'
>>> arg3.value
'cherry preserves'

You can use the value property to access the values of arg2 and arg3 and see that they have changed following the call to the method.


While passing arguments by reference is a feature of ObjectScript methods, there is no equivalent way to pass arguments by reference to a method written in Python. The ByRef keyword in the signature of an ObjectScript method is just a convention used to indicate to the user that the method expects that an argument is to be passed by reference. In fact, ByRef has no actual function and is ignored by the compiler. Adding ByRef to the signature of a method written in Python has no effect.

Passing Values for True, False, and None

The %SYS.PythonOpens in a new tab class has the methods True(), False(), and None(), which represent the Python identifiers True, False, and None, respectively.

For example:

USER>zwrite ##class(%SYS.Python).True()
2@%SYS.Python  ; True  ; <OREF>

These methods are useful if you need to pass True, False, and None to a Python method. The following example uses the method shown in Keyword or Named Arguments.

USER>do obj.mymethod(##class(%SYS.Python).True(), ##class(%SYS.Python).False(), ##class(%SYS.Python).None())
foo=True, bar=False, baz=None

If you pass unnamed arguments to a Python method that expects keyword arguments, Python handles them in the order they are passed in.

Note that you do not need to use the methods True(), False(), and None() when examining the values returned by a Python method to ObjectScript.

Say the Python module mymodule also has a method isgreaterthan(), which is defined as follows:

def isgreaterthan(a, b):
    return a > b

When run in Python, you can see that the method returns True if the argument a is greater than b, and False otherwise:

>>> mymodule.isgreaterthan(5, 4)

However, when called from ObjectScript, the returned value is 1, not the Python identifier True:

USER>zwrite obj.isgreaterthan(5, 4)


In Python, dictionaries are commonly used to store data in key/value pairs, for example:

>>> mycar = {
...     "make": "Toyota",
...     "model": "RAV4",
...     "color": "blue"
... }
>>> print(mycar)
{'make': 'Toyota', 'model': 'RAV4', 'color': 'blue'}
>>> print(mycar["color"])

On the ObjectScript side, you can manipulate Python dictionaries using the dict() method of the Python builtins module:

USER>set mycar = ##class(%SYS.Python).Builtins().dict()
USER>do mycar.setdefault("make", "Toyota")
USER>do mycar.setdefault("model", "RAV4")
USER>do mycar.setdefault("color", "blue")
USER>zwrite mycar
mycar=2@%SYS.Python  ; {'make': 'Toyota', 'model': 'RAV4', 'color': 'blue'}  ; <OREF>
USER>write mycar."__getitem__"("color")

The example above uses the dictionary method setdefault() to set the value of a key and __getitem__() to get the value of a key.


In Python, lists store collections of values, but without keys. Items in a list are accessed by their index.

>>> fruits = ["apple", "banana", "cherry"]
>>> print(fruits)
['apple', 'banana', 'cherry']
>>> print(fruits[0])

In ObjectScript, you can work with Python lists using the list() method of the Python builtins module:

USER>set l = ##class(%SYS.Python).Builtins().list()
USER>do l.append("apple")
USER>do l.append("banana")
USER>do l.append("cherry")
USER>zwrite l
l=13@%SYS.Python  ; ['apple', 'banana', 'cherry']  ; <OREF>
USER>write l."__getitem__"(0)

The example above uses the list method append() to append an item to the list and __getitem__() to get the value at a given index. (Python lists are zero based.)


Most of the time, you will probably access data stored in InterSystems IRIS either by using SQL or by using persistent classes and their properties and methods. However, there may be times when you want to directly access the underlying native persistent data structures, called globals. This is particularly true if you are accessing legacy data or if you are storing schema-less data that doesn’t lend itself to SQL tables or persistent classes.

Though it is an oversimplification, you can think of a global as a dictionary of key/value pairs. (See Introduction to Globals for a more accurate description.)

Consider the following class, which has two class methods written in Python:

Class User.Globals
ClassMethod SetSquares(x) [ Language = python ]
    import iris
    square = iris.gref("^square")
    for key in range(1, x):
        value = key * key
        square.set([key], value)
ClassMethod PrintSquares() [ Language = python ]
    import iris
    square = iris.gref("^square")
    key = ""
    while True:
        key = square.order([key])
        if key == None:
        print("The square of " + str(key) + " is " + str(square.get([key])))

The method SetSquares() loops over a range of keys, storing the square of each key at each node of the global ^square. The method PrintSquares() traverses the global and prints each key and the value stored at the key.

Let’s launch the Python shell, instantiate the class, and run the code to see how it works.

USER>do ##class(%SYS.Python).Shell()
Python 3.9.5 (default, May 31 2022, 12:35:47) [MSC v.1927 64 bit (AMD64)] on win32
Type quit() or Ctrl-D to exit this shell.
>>> g = iris.cls('User.Globals')
>>> g.SetSquares(6)
>>> g.PrintSquares()
The square of 1 is 1
The square of 2 is 4
The square of 3 is 9
The square of 4 is 16
The square of 5 is 25

Now, let’s look at how some of the methods of the built-in iris module allow us to access globals.

In method SetSquares(), the statement square = iris.gref("^square") returns a reference to the global ^square, also known as a gref:

>>> square = iris.gref("^square")

The statement square.set([key], value) sets the node of ^square with key key to the value value, for example you can set node 12 of ^square to the value 144:

>>> square.set([12], 144)

You can also set the node of a global with the following shorter syntax:

>>> square[13] = 169

In method PrintSquares(), the statement key = square.order([key]) takes a key as input and returns the next key in the global, similar to the $ORDER function in ObjectScript. A common technique for a traversing a global is to continue using order() until it returns None, indicating that no more keys remain. Keys do not need to be consecutive, so order() returns the next key even if there are gaps between keys:

>>> key = 5
>>> key = square.order([key])
>>> print(key)

Then, square.get([key]) takes a key as input and returns the value at that key in the global:

>>> print(square.get([key]))

Again, you can use the following shorter syntax:

>>> print(square[13])

Note that nodes in a global don’t have to have a key. The following statement stores a string at the root node of ^square:

>>> square[None] = 'Table of squares'

To show that these Python commands did in fact store values in the global, exit the Python shell and then use the zwrite command in ObjectScript to print the contents of ^square:

>>> quit()
USER>zwrite ^square
^square="Table of squares"

From the Python shell, type help(iris) for the complete list of methods that can be used on a global reference.

Changing Namespaces

InterSystems IRIS has the concept of namespaces, each of which has its own databases for storing code and data. This makes it easy to keep the code and data of one namespace separate from the code and data of another namespace. For example, if one namespace has a global with a certain name, another namespace can use a global with the same name without the danger of conflicting with the other global.

If you have two namespaces, NSONE and NSTWO, you could create a global called ^myFavorite in NSONE, using ObjectScript in Terminal, as shown below. Then you could set the $namespace special variable to change to NSTWO and create a separate global called ^myFavorite in that namespace. (To replicate this example, you can configure these two namespaces on your InterSystems IRIS instance or use two namespaces you already have.)

NSONE>set ^myFavorite("fruit") = "apple"
NSONE>set $namespace = "NSTWO"
NSTWO>set ^myFavorite("fruit") = "orange"

Here, ^myFavorite("fruit") has the value "apple" in NSONE and the value "orange" in NSTWO.

When you call Embedded Python, it inherits the current namespace. We can test this by calling the NameSpace() method of the %SYSTEM.SYSOpens in a new tab class from Python, which displays the name of the current namespace, and by confirming that ^myFavorite("fruit") = "orange".

NSTWO>do ##class(%SYS.Python).Shell()
Python 3.9.5 (default, Jun  2 2023, 14:12:21) [MSC v.1927 64 bit (AMD64)] on win32
Type quit() or Ctrl-D to exit this shell.
>>> iris.cls('%SYSTEM.SYS').NameSpace()
>>> myFav = iris.gref('^myFavorite')
>>> print(myFav['fruit'])

You’ve seen how to use $namespace to change namespaces in ObjectScript. In Embedded Python, you use the SetNamespace() method of the iris.system.Process class. For example, you can change to the namespace NSONE and confirm that ^myFavorite("fruit") = "apple".

>>> iris.system.Process.SetNamespace('NSONE')
>>> myFav = iris.gref('^myFavorite')
>>> print(myFav['fruit'])

Finally, when you exit from the Python shell, you remain in namespace NSONE.

>>> quit()

Running an ObjectScript Command from Embedded Python

There are times you may want to run an ObjectScript command from Embedded Python, for example, to access a system-provided “special variable”, to call a routine written in ObjectScript, or to perform other tasks where there is no available method to call. In such cases, you can use the methods iris.execute() and iris.routine() from Python.

The following example writes the special variable $zversion, which contains the InterSystems IRIS version string:

>>> iris.execute("write $zversion,!")
IRIS for Windows (x86-64) 2022.3 (Build 602U) Mon Jan 23 2023 14:05:04 EST

Sometimes you may want to return a value from iris.execute() and assign it to a Python variable. This example assigns the value of the special variable $horolog, which contains the local date and time for the current process in InterSystems IRIS internal storage format, to the variable t:

>>> t = iris.execute("return $horolog")
>>> t

This is equivalent to t = iris.cls('%SYSTEM.SYS').Horolog(), which uses the Horolog() method of the class %SYSTEM.SYSOpens in a new tab.

You may encounter older ObjectScript code that uses routines instead of classes and methods and want to call a routine from Embedded Python. If you have a routine ^Math that has a function Sum() that returns the sum of two numbers, you can add two numbers and assign the return value to the Python variable sum, as follows:

>>> sum = iris.execute("return $$Sum^Math(4,3)")
>>> sum
>>> 7

While the recommended way to access globals from Embedded Python is to use the iris.gref() method, you can also set and retrieve values from a global by using iris.execute(). The following example sets the global ^motd to the value "hello world" and then retrieves the value from the global.

>>> iris.execute("set ^motd = \"hello world\"")
>>> iris.execute("return ^motd")
'hello world'

You can also call routines by using the iris.routine() method. The following example, when run in the %SYS namespace, calls the routine ^SECURITY:

>>> iris.routine('^SECURITY')
1) User setup
2) Role setup
3) Service setup
4) Resource setup

The example given above of calling the Sum() function of a hypothetical ^Math routine can also be done with the iris.routine() method. The following example adds the numbers 4 and 3:

>>> sum = iris.routine('Sum^Math',4,3)
>>> sum

Exception Handling

The InterSystems IRIS exception handler can handle Python exceptions and pass them seamlessly to ObjectScript. Building on the earlier Python library example, if you try to call canvas.drawImage() using a non-existent file, and catch the exception in ObjectScript, you see the following:

USER>try { do canvas.drawImage("C:\Sample\bad.png", 150, 600) } catch { write "Error: ", $zerror, ! }
Error: <THROW> *%Exception.PythonException <THROW> 230 ^^0^DO canvas.drawImage("W:\Sample\isc.png", 150, 600) 
<class 'OSError'>: Cannot open resource "W:\Sample\isc.png" -

Here, <class 'OSError'>: Cannot open resource "W:\Sample\isc.png" is the exception passed back from Python.

Bytes and Strings

Python draws a clear distinction between objects of the “bytes” data type, which are simply sequences of 8-bit bytes, and strings, which are sequences of UTF-8 bytes that represent a string. In Python, bytes objects are never converted in any way, but strings might be converted depending on the character set in use by the host operating system, for example, Latin-1.

InterSystems IRIS makes no distinction between bytes and strings. While InterSystems IRIS supports Unicode strings (UCS-2/UTF-16), any string that contains values of less than 256 could either be a string or bytes. For this reason, the following rules apply when passing strings and bytes to and from Python:

  • InterSystems IRIS strings are assumed to be strings and are converted to UTF-8 when passed from ObjectScript to Python.

  • Python strings are converted from UTF-8 to InterSystems IRIS strings when passed back to ObjectScript, which may result in wide characters.

  • Python bytes objects are returned to ObjectScript as 8-bit strings. If the length of the bytes object exceeds the maximum string length, then a Python bytes object is returned.

  • To pass bytes objects to Python from ObjectScript, use the ##class(%SYS.Python).Bytes() method, which does not convert the underlying InterSystems IRIS string to UTF-8.

The following example turns an InterSystems IRIS string to a Python object of type bytes:

USER>set b = ##class(%SYS.Python).Bytes("Hello Bytes!")
USER>zwrite b
b=8@%SYS.Python  ; b'Hello Bytes!'  ; <OREF>
USER>zwrite builtins.type(b)
4@%SYS.Python  ; <class 'bytes'>  ; <OREF>

To construct Python bytes objects bigger than the 3.8MB maximum string length in InterSystems IRIS, you can use a bytearray object and append smaller chunks of bytes using the extend() method. Finally, pass the bytearray object into the builtins bytes() method to get a bytes representation:

USER>set ba = builtins.bytearray()
USER>do ba.extend(##class(%SYS.Python).Bytes("chunk 1"))
USER>do ba.extend(##class(%SYS.Python).Bytes("chunk 2"))
USER>zwrite builtins.bytes(ba)
"chunk 1chunk 2"

Standard Output and Standard Error Mappings

When using Embedded Python, standard output is mapped to the InterSystems IRIS console, which means that the output of any print() statements is sent to the Terminal. Standard error is mapped to the InterSystems IRIS messages.log file, located in the directory <install-dir>/mgr.

As an example, consider this Python method:

def divide(a, b):
    except ZeroDivisionError:
        print("Cannot divide by zero")
    except TypeError:
        import sys
        print("Bad argument type", file=sys.stderr)
        print("Something else went wrong")

If you test this method in Terminal, you might see the following:

USER>set obj = ##class(%SYS.Python).Import("mymodule")
USER>do obj.divide(5, 0)
Cannot divide by zero
USER>do obj.divide(5, "hello")

If you try to divide by zero, the error message is directed to the Terminal, but if you try to divide by a string, the message is sent to messages.log:

11/19/21-15:49:33:248 (28804) 0 [Python] Bad argument type

Only important messages should be sent to messages.log, to avoid cluttering the file.

FeedbackOpens in a new tab