Calling Mojo from Python
If you have an existing Python project that would benefit from Mojo's high-performance computing, you shouldn't have to rewrite the whole thing in Mojo. Instead, you can write just the performance-critical parts your code in Mojo and then call it from Python.
Import a Mojo module in Pythonโ
To illustrate what calling Mojo from Python looks like, we'll start with a simple example, and then dig into the details of how it works and what is possible today.
Consider a project with the following structure:
project
โโโ ๐ main.py
โโโ ๐ฅ mojo_module.mojo
project
โโโ ๐ main.py
โโโ ๐ฅ mojo_module.mojo
The main entrypoint is a Python program called main.py
, and the Mojo code
includes functions to call from Python.
For example, let's say we want a Mojo function to take a Python value as an argument:
fn factorial(py_obj: PythonObject) raises -> Python
var n = Int(py_obj)
return math.factorial(n)
fn factorial(py_obj: PythonObject) raises -> Python
var n = Int(py_obj)
return math.factorial(n)
And we want to call it from Python like this:
import mojo_module
print(mojo_module.factorial(5))
import mojo_module
print(mojo_module.factorial(5))
However, before we can call the Mojo function from Python, we must declare it so Python knows it exists.
Because Python is trying to load mojo_module
, it looks for a function called
PyInit_mojo_module()
. (If our file was called foo.mojo
, the function Python
looked for would be PyInit_foo()
.) Within the PyInit_mojo_module()
, we must
declare all Mojo functions and types that are callable from Python using
PythonModuleBuilder
.
So the complete Mojo code looks like this:
from python import PythonObject
from python.bindings import PythonModuleBuilder
import math
from os import abort
@export
fn PyInit_mojo_module() -> PythonObject:
try:
var m = PythonModuleBuilder("mojo_module")
m.def_function[factorial]("factorial", docstring="Compute n!")
return m.finalize()
except e:
return abort[PythonObject](String("error creating Python Mojo module:", e))
fn factorial(py_obj: PythonObject) raises -> PythonObject:
# Raises an exception if `py_obj` is not convertible to a Mojo `Int`.
var n = Int(py_obj)
return math.factorial(n)
from python import PythonObject
from python.bindings import PythonModuleBuilder
import math
from os import abort
@export
fn PyInit_mojo_module() -> PythonObject:
try:
var m = PythonModuleBuilder("mojo_module")
m.def_function[factorial]("factorial", docstring="Compute n!")
return m.finalize()
except e:
return abort[PythonObject](String("error creating Python Mojo module:", e))
fn factorial(py_obj: PythonObject) raises -> PythonObject:
# Raises an exception if `py_obj` is not convertible to a Mojo `Int`.
var n = Int(py_obj)
return math.factorial(n)
On the Python side, we add the directory containing mojo_module.mojo
to the
Python path, and then use a normal import
statement to load our Mojo code:
import max.mojo.importer
import sys
sys.path.insert(0, "")
import mojo_module
print(mojo_module.factorial(5))
import max.mojo.importer
import sys
sys.path.insert(0, "")
import mojo_module
print(mojo_module.factorial(5))
That's it! Try it:
python main.py
python main.py
120
120
How it worksโ
Python supports a standard mechanism called Python extension
modules that enables
compiled languages (like Mojo, C, C++, or Rust) to make themselves callable
from Python in an intuitive way. Concretely, a Python extension module is
simply a dynamic library that defines a suitable PyInit_*()
function.
Mojo comes with built-in functionality for defining Python extension modules.
The special stuff happens in the max.mojo.importer
module we imported.
If we have a look at the filesystem after Python imports the Mojo code, we'll
notice there's a new __mojocache__
directory, with dynamic library (.so
)
file inside:
project
โโโ main.py
โโโ mojo_module.mojo
โโโ __mojocache__
โโโ mojo_module.hash-ABC123.so
project
โโโ main.py
โโโ mojo_module.mojo
โโโ __mojocache__
โโโ mojo_module.hash-ABC123.so
Loading max.mojo.importer
loads our Python Mojo import
hook, which
behind the scenes looks for a .mojo
(or .๐ฅ
) file that matches the imported
module name, and if found, compiles it using mojo build --emit shared-lib
to generate a static library.
The resulting file is stored in __mojocache__
, and is rebuilt only when
it becomes stale (typically, when the Mojo source file changes).
Now that we've looked at the basics of how Mojo can be used from Python, let's dig into the available features and how you can leverage them to accelerate your Python with Mojo.
Bindings featuresโ
Binding Mojo typesโ
You can bind any Mojo type for use in Python using
PythonModuleBuilder
.
For example:
@fieldwise_init
struct Person(Movable, Representable):
var name: String
var age: Int
fn __repr__(self) -> String:
return String("Person(", self.name, ", ", self.age, ")")
@export
fn PyInit_person_module() -> PythonObject:
try:
var mb = PythonModuleBuilder("person_module")
var person_type = mb.add_type[Person]("Person")
except e:
return abort[PythonObject]("error creating Mojo module")
@fieldwise_init
struct Person(Movable, Representable):
var name: String
var age: Int
fn __repr__(self) -> String:
return String("Person(", self.name, ", ", self.age, ")")
@export
fn PyInit_person_module() -> PythonObject:
try:
var mb = PythonModuleBuilder("person_module")
var person_type = mb.add_type[Person]("Person")
except e:
return abort[PythonObject]("error creating Mojo module")
When you call
add_type()
, it
returns a
PythonTypeBuilder
, which
you can then use to bind the type constructor (see binding Python
initializers, below) and methods.
Any Mojo type bound using a PythonTypeBuilder
has the resulting Python
'type' object globally registered, enabling two features:
-
Constructing Python objects that wrap Mojo values for use from Python using
PythonObject(alloc=Person(..))
. -
Downcasting using
python_obj.downcast_value_ptr[Person]()
However, merely binding a Mojo type to a Python type
object isnโt very useful on its own. Next, weโll tell Python how to interact with our Mojo typeโstarting with how to construct instances of our Mojo type from within Python.
Constructing Mojo objects in Pythonโ
Mojo types can be constructed from Python by declaring a Mojo constructor function as a Python-compatible object initializer using
def_py_init()
when you add the type to your module. For example:
@export
fn PyInit_person_module() -> PythonObject:
try:
var mb = PythonModuleBuilder("person_module")
_ = mb.add_type[Person]("Person").def_py_init[Person.py_init]()
return mb.finalize()
except e:
return abort[PythonObject](
String("error creating Python Mojo module:", e)
)
@fieldwise_init
struct Person(Movable, Representable):
var name: String
var age: Int
fn __repr__(self) -> String:
return String("Person(", self.name, ", ", self.age, ")")
@staticmethod
fn py_init(
out self: Person, args: PythonObject, kwargs: PythonObject
) raises:
# Validate argument count
if len(args) != 2:
raise Error("Person() takes exactly 2 arguments")
# Convert Python arguments to Mojo types
var name = String(args[0])
var age = Int(args[1])
self = Self(name, age)
@export
fn PyInit_person_module() -> PythonObject:
try:
var mb = PythonModuleBuilder("person_module")
_ = mb.add_type[Person]("Person").def_py_init[Person.py_init]()
return mb.finalize()
except e:
return abort[PythonObject](
String("error creating Python Mojo module:", e)
)
@fieldwise_init
struct Person(Movable, Representable):
var name: String
var age: Int
fn __repr__(self) -> String:
return String("Person(", self.name, ", ", self.age, ")")
@staticmethod
fn py_init(
out self: Person, args: PythonObject, kwargs: PythonObject
) raises:
# Validate argument count
if len(args) != 2:
raise Error("Person() takes exactly 2 arguments")
# Convert Python arguments to Mojo types
var name = String(args[0])
var age = Int(args[1])
self = Self(name, age)
With this Mojo binding, you can create Person
instances in Python:
person = person_module.Person("Sarah", 32)
print(person)
person = person_module.Person("Sarah", 32)
print(person)
Person(Sarah, 32)
Person(Sarah, 32)
For types that support default construction, you can use the simpler
def_init_defaultable()
method:
var counter_type = m.add_type[Counter]("Counter")
counter_type.def_init_defaultable[Counter]()
var counter_type = m.add_type[Counter]("Counter")
counter_type.def_init_defaultable[Counter]()
This enables Python code to create instances without arguments:
counter = counter_module.Counter() # Creates Counter()
counter = counter_module.Counter() # Creates Counter()
Returning Mojo objects to Pythonโ
Mojo functions called from Python don't just need to be able to accept
PythonObject
values as
arguments, they also need to be able to return new values. And sometimes, they
even need to be able to return Mojo native values back to Python. This is
possible by using the PythonObject(alloc=<value>)
constructor.
An example of this looks like:
fn create_person() -> PythonObject:
var person = Person("Sarah", 32)
return PythonObject(alloc=person^)
fn create_person() -> PythonObject:
var person = Person("Sarah", 32)
return PythonObject(alloc=person^)
PythonObject
to Mojo valuesโ
Within any Mojo code that is handling a
PythonObject
, but
especially within Mojo functions called from Python, it's common to expect an
argument of a particular type.
There are two ways in which a PythonObject
can be turned into a native
Mojo value:
-
Converting a Python object into a newly constructed Mojo value that has the same logical value as the original Python object. This is handled by the
ConvertibleFromPython
trait. -
Downcasting a Python object that holds a native Mojo value to a pointer to that inner value. This is handled by
PythonObject.downcast_value_ptr()
.
PythonObject
conversionsโ
Many Mojo types support conversion directly from equivalent Python types, via
the ConvertibleFromPython
trait:
# Given a person, clone them and give them a different name.
fn create_person(
name_obj: PythonObject,
age_obj: PythonObject
) raises -> PythonObject:
# These conversions will raise an exception if they fail
var name = String(name_obj)
var age = Int(age_obj)
return PythonObject(alloc=Person(name, age))
# Given a person, clone them and give them a different name.
fn create_person(
name_obj: PythonObject,
age_obj: PythonObject
) raises -> PythonObject:
# These conversions will raise an exception if they fail
var name = String(name_obj)
var age = Int(age_obj)
return PythonObject(alloc=Person(name, age))
Which could be called from Python using:
person = mojo_module.create_person("John Smith")
person = mojo_module.create_person("John Smith")
Passing invalid arguments will result in a runtime argument error:
person = mojo_module.create_person(42)
person = mojo_module.create_person(42)
PythonObject
downcastsโ
Downcasting from PythonObject
values to the inner Mojo value:
fn print_age(person_obj: PythonObject) raises:
# Raises if `obj` does not contain an instance of the Mojo `Person` type.
var person = person_obj.downcast_value_ptr[Person]()
print("Person is", person[].age, "years old")
fn print_age(person_obj: PythonObject) raises:
# Raises if `obj` does not contain an instance of the Mojo `Person` type.
var person = person_obj.downcast_value_ptr[Person]()
print("Person is", person[].age, "years old")
Unsafe mutation via downcasting is also supported. It is up to the user to ensure that this mutable pointer does not alias any other pointers to the same object within Mojo:
fn birthday(person_obj: PythonObject):
var person = person_obj.downcast_value_ptr[Person]()
person[].age += 1
fn birthday(person_obj: PythonObject):
var person = person_obj.downcast_value_ptr[Person]()
person[].age += 1
Entirely unchecked downcastingโwhich does no type checkingโcan be done using:
fn get_person(person_obj: PythonObject):
var person = person_obj.unchecked_downcast_value_ptr[Person]()
fn get_person(person_obj: PythonObject):
var person = person_obj.unchecked_downcast_value_ptr[Person]()
Unchecked downcasting can be used to eliminate overhead when optimizing a tight inner loop with Mojo, and you've benchmarked and measured that type checking downcasts is a significant bottleneck.
Methodsโ
When binding Mojo objects for use from Python, you can expose chosen methods to
Python as well, using PythonTypeBuilder.def_method()
.
Currently, Mojo methods being exposed to Python must be written with a modification
compared to normal Mojo methods: they must be a @staticmethod
that takes
either py_self: PythonObject
or self_ptr: UnsafePointer[Self]
:
from python import PythonObject
from python.bindings import PythonModuleBuilder
from os import abort
@export
fn PyInit_mojo_module() -> PythonObject:
try:
var mb = PythonModuleBuilder("mojo_module")
_ = mb.add_type[Person]("Person")
.def_method[Person.get_name]("get_name")
.def_method[Person.set_age]("set_age")
return mb.finalize()
except e:
return abort[PythonObject]("error creating Mojo module")
struct Person(Representable):
var name: String
var age: Int
@staticmethod
fn get_name(py_self: PythonObject) raises -> PythonObject:
var self_ptr = py_self.downcast_value_ptr[Self]()
return self_ptr[].name
@staticmethod
fn set_age(self_ptr: UnsafePointer[Self], new_age: PythonObject) raises:
self_ptr[].age = Int(new_age)
fn __repr__(self) -> String:
return String("Person(", self.name, ", ", self.age, ")")
from python import PythonObject
from python.bindings import PythonModuleBuilder
from os import abort
@export
fn PyInit_mojo_module() -> PythonObject:
try:
var mb = PythonModuleBuilder("mojo_module")
_ = mb.add_type[Person]("Person")
.def_method[Person.get_name]("get_name")
.def_method[Person.set_age]("set_age")
return mb.finalize()
except e:
return abort[PythonObject]("error creating Mojo module")
struct Person(Representable):
var name: String
var age: Int
@staticmethod
fn get_name(py_self: PythonObject) raises -> PythonObject:
var self_ptr = py_self.downcast_value_ptr[Self]()
return self_ptr[].name
@staticmethod
fn set_age(self_ptr: UnsafePointer[Self], new_age: PythonObject) raises:
self_ptr[].age = Int(new_age)
fn __repr__(self) -> String:
return String("Person(", self.name, ", ", self.age, ")")
Taking py_self: PythonObject
allows access to the full PythonObject
allocation that a Mojo object instance is stored inside of. Typically though, taking py_self: UnsafePointer[Self]
will minimize boilerplate in the common case that a method merely needs to access the fields of an object.
Mojo methods called from Python are currently required to take non-standard self types due to limitations that will be lifted in future versions of Python Mojo bindings.
Static methodsโ
Python Mojo bindings supports exposing Python @staticmethods
, bound using PythonTypeBuilder.def_staticmethod()
. A function declared using def_staticmethod()
is callable as a static method on the type within Python, without needing an object instance.
from python import PythonObject
from python.bindings import PythonModuleBuilder
from os import abort
@export
fn PyInit_mojo_module() -> PythonObject:
try:
var mb = PythonModuleBuilder("mojo_module")
mb.add_type[Person]("Person")
.def_staticmethod[Person.is_valid_age]("is_valid_age")
return mb.finalize()
except e:
return abort[PythonObject]("error creating Mojo module")
struct Person(Representable):
var name: String
var age: Int
@staticmethod
fn is_valid_age(age_obj: PythonObject) raises -> PythonObject:
var age = Int(age_obj)
return 0 <= age <= 130
fn __repr__(self) -> String:
return String("Person(", self.name, ", ", self.age, ")")
from python import PythonObject
from python.bindings import PythonModuleBuilder
from os import abort
@export
fn PyInit_mojo_module() -> PythonObject:
try:
var mb = PythonModuleBuilder("mojo_module")
mb.add_type[Person]("Person")
.def_staticmethod[Person.is_valid_age]("is_valid_age")
return mb.finalize()
except e:
return abort[PythonObject]("error creating Mojo module")
struct Person(Representable):
var name: String
var age: Int
@staticmethod
fn is_valid_age(age_obj: PythonObject) raises -> PythonObject:
var age = Int(age_obj)
return 0 <= age <= 130
fn __repr__(self) -> String:
return String("Person(", self.name, ", ", self.age, ")")
Calling a Mojo function bound as a static method looks like a typical Python static method call directly on the type object:
from mojo_module import Person
print(Person.is_valid_age(45)) # Prints 'True'
print(Person.is_valid_age(-1)) # Prints 'False'
from mojo_module import Person
print(Person.is_valid_age(45)) # Prints 'True'
print(Person.is_valid_age(-1)) # Prints 'False'
Keyword argumentsโ
Keyword arguments in Mojo come in two forms:
- Keyword-only arguments:
fn foo(*, x: Int)
This is not currently supported in Python Mojo bindings. - Variadic keyword arguments:
fn foo(**kwargs: Int)
This is supported in Python Mojo bindings when used in the unsugared form:fn foo(kwargs: OwnedKwargsDict)
. (The**kwargs
syntax limitation will be removed in the future.)
You can define Mojo functions that accept variadic keyword arguments using OwnedKwargsDict[PythonObject]
as the last argument. A simple example looks like:
import mojo_module
result = mojo_module.sum_kwargs_ints(a=10, b=20, c=30) # returns 60
import mojo_module
result = mojo_module.sum_kwargs_ints(a=10, b=20, c=30) # returns 60
from collections import OwnedKwargsDict
def sum_kwargs_ints(kwargs: OwnedKwargsDict[PythonObject]) -> PythonObject:
var total = 0
for entry in kwargs.items():
total += Int(entry.value)
return PythonObject(total)
from collections import OwnedKwargsDict
def sum_kwargs_ints(kwargs: OwnedKwargsDict[PythonObject]) -> PythonObject:
var total = 0
for entry in kwargs.items():
total += Int(entry.value)
return PythonObject(total)
Keyword arguments are also supported following normal positional arguments. Additionally, getting specific keyword arguments is a dictionary lookup on the OwnedKwargsDict
:
from collections import OwnedKwargsDict
def duration_in_seconds(
hours_obj: PythonObject,
minutes_obj: PythonObject,
kwargs: OwnedKwargsDict[PythonObject]
) -> PythonObject:
var hours = Int(hours_obj)
var minutes = Int(minutes_obj)
var seconds = Int(kwargs["seconds"])
return hours * 3600 + minutes * 60 + seconds
from collections import OwnedKwargsDict
def duration_in_seconds(
hours_obj: PythonObject,
minutes_obj: PythonObject,
kwargs: OwnedKwargsDict[PythonObject]
) -> PythonObject:
var hours = Int(hours_obj)
var minutes = Int(minutes_obj)
var seconds = Int(kwargs["seconds"])
return hours * 3600 + minutes * 60 + seconds
In this example, if a call to duration_in_seconds()
is missing the required "seconds"
named argument, a runtime exception will occur:
from mojo_module import duration_in_seconds
# Pass hours and minutes, missing "seconds"
duration_in_seconds(4, 5) # ERROR: KeyError
from mojo_module import duration_in_seconds
# Pass hours and minutes, missing "seconds"
duration_in_seconds(4, 5) # ERROR: KeyError
Keyword arguments are supported when bindings top-level functions, methods, and static methods.
Variadic argumentsโ
Python and Mojo variadic arguments are normally written using the following syntax:
fn foo(*args: Int):
...
fn foo(*args: Int):
...
However, this syntax is not yet supported in Python/Mojo bindings, because functions bound using
def_function()
support only fixed-arity functions.
As a workaround, you can expose Mojo functions that accept
a variadic number of arguments to Python using the lower-level
def_py_function()
interface, which leaves it to the user to validate the number of arguments
provided:
@export
fn PyInit_mojo_module() -> PythonObject:
try:
var b = PythonModuleBuilder("mojo_module")
b.def_py_function[count_args]("count_args")
b.def_py_function[sum_args]("sum_args")
b.def_py_function[lookup]("lookup")
fn count_args(py_self: PythonObject, args_tuple: PythonObject) raises:
return len(args_tuple)
fn sum_args(py_self: PythonObject, args_tuple: PythonObject) raises:
var total = args_tuple[0]
for i in range(1, len(args_tuple)):
total += args_tuple[i]
return total
fn lookup(py_self: PythonObject, args_tuple: PythonObject) raises:
if len(args_tuple) != 2 and len(args_tuple) != 3:
raise Error("lookup() expects 2 or 3 arguments")
var collection = args_tuple[0]
var key = args_tuple[1]
try:
return collection[key]
except e:
if len(args) == 3:
return args_tuple[2]
else:
raise e
@export
fn PyInit_mojo_module() -> PythonObject:
try:
var b = PythonModuleBuilder("mojo_module")
b.def_py_function[count_args]("count_args")
b.def_py_function[sum_args]("sum_args")
b.def_py_function[lookup]("lookup")
fn count_args(py_self: PythonObject, args_tuple: PythonObject) raises:
return len(args_tuple)
fn sum_args(py_self: PythonObject, args_tuple: PythonObject) raises:
var total = args_tuple[0]
for i in range(1, len(args_tuple)):
total += args_tuple[i]
return total
fn lookup(py_self: PythonObject, args_tuple: PythonObject) raises:
if len(args_tuple) != 2 and len(args_tuple) != 3:
raise Error("lookup() expects 2 or 3 arguments")
var collection = args_tuple[0]
var key = args_tuple[1]
try:
return collection[key]
except e:
if len(args) == 3:
return args_tuple[2]
else:
raise e
Strategies for porting Python to Mojoโ
Writing Pythonic code in Mojoโ
In this approach to bindings, we embrace the flexibility of Python, and eschew
trying to convert PythonObject
arguments into the narrowly constrained,
strongly-typed space of the Mojo type system, in favor of just writing some code
and letting it raise an exception at runtime if we got something wrong.
The flexibility of PythonObject
enables a unique programming style, wherein
Python code can be "ported" to Mojo with relatively few changes.
def foo(x, y, z):
x[y] = int(z)
x = y + z
def foo(x, y, z):
x[y] = int(z)
x = y + z
Rule of thumb: Any Python builtin function should be accessible in Mojo using
Python.<builtin>()
.
fn foo(x: PythonObject, y: PythonObject, z: PythonObject) -> PythonObject:
x[y] = Python.int(z)
x = y + z
fn foo(x: PythonObject, y: PythonObject, z: PythonObject) -> PythonObject:
x[y] = Python.int(z)
x = y + z
Building Mojo extension modulesโ
You can create and distribute your Mojo modules for Python in the following ways:
-
As source files, compiled on demand using the Python Mojo importer hook.
The advantage of this approach is that it's easy to get started with, and keeps your project structure simple, while ensuring that your imported Mojo code is always up to date after you make an edit.
-
As pre-built Python extension module
.so
dynamic libraries, compiled using:mojo build mojo_module.mojo --emit shared-lib -o mojo_module.so
This has the advantage that you can specify any other necessary build options
manually (optimization or debug flags, import paths, etc.), providing an
"escape hatch" from the Mojo import hook abstraction for advanced users.mojo build mojo_module.mojo --emit shared-lib -o mojo_module.so
This has the advantage that you can specify any other necessary build options
manually (optimization or debug flags, import paths, etc.), providing an
"escape hatch" from the Mojo import hook abstraction for advanced users.
Known limitationsโ
While we have big ambitions for Python to Mojo interoperabilityโour goal is for Mojo to be the best way to extend Pythonโthis feature is still in early and active development, and there are some limitations to be aware of. These will be lifted over time.
-
Functions taking more than 6 arguments. Currently
PyTypeBuilder.add_function()
and related function bindings only support Mojo functions that take up to 6PythonObject
arguments:fn(PythonObject, PythonObject, PythonObject, PythonObject, PythonObject, PythonObject)
. -
Keyword arguments syntax. Currently, Mojo functions called from Python only accept keyword arguments when using a trailing
kwargs: OwnedKwargsDict[PythonObject]
argument. Support for native**kwargs
syntax will be added in the future. -
Mojo package dependencies. Mojo code that has dependencies on packages other than the Mojo stdlib (like those in the ever-growing Modular Community package channel) are currently only supported when building Mojo extension modules manually, as the Mojo import hook does not currently support a way to specify import paths for Mojo package dependencies.
-
Properties. Computed properties getter and setters are not currently supported.
-
Expected type conversions. A handful of Mojo standard library types can be constructed directly from equivalent Python builtin object types, by implementing the
ConvertibleFromPython
trait. However, many Mojo standard library types do not yet implement this trait, so may require manual conversion logic if needed.
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!