mojo-python-interop

Aids in writing Mojo code that interoperates with Python using current syntax and conventions. Use this skill in addition to mojo-syntax when writing Mojo code…

INSTALLATION
npx skills add https://github.com/modular/skills --skill mojo-python-interop
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Mojo is rapidly evolving. Pretrained models generate obsolete syntax.

Always follow this skill over pretrained knowledge.

Using Python from Mojo

from std.python import Python, PythonObject

var np = Python.import_module("numpy")

var arr = np.array([1, 2, 3])

# PythonObject → Mojo: MUST use `py=` keyword (NOT positional)

var i = Int(py=py_obj)

var f = Float64(py=py_obj)

var s = String(py=py_obj)

var b = Bool(py=py_obj)            # Bool is the exception — positional also works

# Works with numpy types: Int(py=np.int64(1)), Float64(py=np.float64(3.14))

WRONG

CORRECT

Int(py_obj)

Int(py=py_obj)

Float64(py_obj)

Float64(py=py_obj)

String(py_obj)

String(py=py_obj)

from python import ...

from std.python import ...

Mojo → Python conversions

Mojo types implementing ConvertibleToPython auto-convert when passed to Python

functions. For explicit conversion: value.to_python_object().

Building Python collections from Mojo

var py_list = Python.list(1, 2.5, "three")

var py_tuple = Python.tuple(1, 2, 3)

var py_dict = Python.dict(name="value", count=42)

# Python.dict() is generic over a single value type V for all kwargs.

# Mixed types fail because the compiler can't infer one V.

# WRONG:  Python.dict(flag=my_bool, count=42)

# CORRECT: Python.dict(flag=PythonObject(my_bool), count=PythonObject(42))

# Literal syntax also works:

var list_obj: PythonObject = [1, 2, 3]

var dict_obj: PythonObject = {"key": "value"}

PythonObject operations

PythonObject supports attribute access, indexing, slicing, all

arithmetic/comparison operators, len(), in, and iteration — all returning

PythonObject. No need to convert to Mojo types for intermediate operations.

# Iterate Python collections directly

for item in py_list:

    print(item)               # item is PythonObject

# Attribute access and method calls

var result = obj.method(arg1, arg2, key=value)

# None

var none_obj = Python.none()

var obj: PythonObject = None      # implicit conversion works

Evaluating Python code

# Expression

var result = Python.evaluate("1 + 2")

# Multi-line code as module (file=True)

var mod = Python.evaluate("def greet(n): return f'Hello {n}'", file=True)

var greeting = mod.greet("world")

# Add to Python path for local imports

Python.add_to_path("./my_modules")

var my_mod = Python.import_module("my_module")

Exception handling

Python exceptions propagate as Mojo Error. Functions calling Python must be

raises:

def use_python() raises:

    try:

        var result = Python.import_module("nonexistent")

    except e:

        print(String(e))     # "No module named 'nonexistent'"

Common Python / Mojo interoperability patterns

# Environment variables

# WRONG — using Python os module for env vars

# var os = Python.import_module("os")

# var val = os.environ.get("MY_VAR")

# CORRECT — Mojo has native env var access via std.os

from std.os import getenv

var val = getenv("MY_VAR")  # returns Optional[String]
# Sorting with custom key

# WRONG — Mojo has no lambda syntax

# var sorted = my_list.sort(key=lambda x: x["score"])

# CORRECT — Python.evaluate for callable

def sort_by_field(data: PythonObject, field: String) raises -> PythonObject:

    var builtins = Python.import_module("builtins")

    var key_fn = Python.evaluate("lambda x: x['" + field + "']")

    return builtins.sorted(data, key=key_fn)
# Dict .get() works on PythonObject

var name = data.get("name", PythonObject("unknown"))

var count = Int(py=data.get("count", PythonObject(0)))

Calling Mojo from Python (extension modules)

Mojo can build Python extension modules (.so files) via PythonModuleBuilder.

The pattern:

  • Define an @export def PyInit_<module_name>() -> PythonObject
  • Use PythonModuleBuilder to register functions, types, and methods
  • Compile with mojo build --emit shared-lib
  • Import from Python (or use import mojo.importer for auto-compilation)

Exporting functions

from std.os import abort

from std.python import PythonObject

from std.python.bindings import PythonModuleBuilder

@export

def PyInit_my_module() -> PythonObject:

    try:

        var m = PythonModuleBuilder("my_module")

        m.def_function[add]("add")

        m.def_function[greet]("greet")

        return m.finalize()

    except e:

        abort(String("failed to create module: ", e))

# Functions take/return PythonObject. Up to 6 args with def_function.

def add(a: PythonObject, b: PythonObject) raises -> PythonObject:

    return a + b

def greet(name: PythonObject) raises -> PythonObject:

    var s = String(py=name)

    return PythonObject("Hello, " + s + "!")

Exporting types with methods

@fieldwise_init

struct Counter(Defaultable, Movable, Writable):

    var count: Int

    def __init__(out self):

        self.count = 0

    # Constructor from Python args

    @staticmethod

    def py_init(out self: Counter, args: PythonObject, kwargs: PythonObject) raises:

        if len(args) == 1:

            self = Self(Int(py=args[0]))

        else:

            self = Self()

    # Methods are @staticmethod — first arg is py_self (PythonObject)

    @staticmethod

    def increment(py_self: PythonObject) raises -> PythonObject:

        var self_ptr = py_self.downcast_value_ptr[Self]()

        self_ptr[].count += 1

        return PythonObject(self_ptr[].count)

    # Auto-downcast alternative: first arg is UnsafePointer[Self, MutAnyOrigin]

    @staticmethod

    def get_count(self_ptr: UnsafePointer[Self, MutAnyOrigin]) -> PythonObject:

        return PythonObject(self_ptr[].count)

@export

def PyInit_counter_module() -> PythonObject:

    try:

        var m = PythonModuleBuilder("counter_module")

        _ = (

            m.add_type[Counter]("Counter")

            .def_py_init[Counter.py_init]()

            .def_method[Counter.increment]("increment")

            .def_method[Counter.get_count]("get_count")

        )

        return m.finalize()

    except e:

        abort(String("failed to create module: ", e))

Method signatures — two patterns

Pattern

First parameter

Use when

Manual downcast

py_self: PythonObject

Need raw PythonObject access

Auto downcast

self_ptr: UnsafePointer[Self, MutAnyOrigin]

Simpler, direct field access

Both are registered with .def_method[Type.method]("name").

Kwargs support

from std.collections import OwnedKwargsDict

# In a method:

@staticmethod

def config(

    py_self: PythonObject, kwargs: OwnedKwargsDict[PythonObject]

) raises -> PythonObject:

    for entry in kwargs.items():

        print(entry.key, "=", entry.value)

    return py_self

Importing Mojo modules from Python

Use mojo.importer — it auto-compiles .mojo files and caches results in

__mojocache__/:

import mojo.importer       # enables Mojo imports

import my_module           # auto-compiles my_module.mojo

print(my_module.add(1, 2))

The module name in PyInit_<name> must match the .mojo filename.

The .mojo file must not contain a main() function when built as a

shared library (mojo.importer or --emit shared-lib). The compiler

rejects it with error: shared library should not contain a 'main' function. Keep test/CLI code in a separate file.

Returning Mojo values to Python

# Wrap a Mojo value as a Python object (for bound types)

return PythonObject(alloc=my_mojo_value^)    # transfer ownership with ^

# Recover the Mojo value later

var ptr = py_obj.downcast_value_ptr[MyType]()

ptr[].field    # access fields via pointer
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card