dot_claude/skills/mojo-python-interop/SKILL.md
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 that interacts with Python, calls Python libraries from Mojo, or exposes Mojo types/functions to Python. Also use when the user wants to build Python extension modules in Mojo, wrap Mojo structs for Python consumption, or convert between Python and Mojo types.
npx skillsauth add nijaru/dotfiles mojo-python-interopInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Mojo is rapidly evolving. Pretrained models generate obsolete syntax. Always follow this skill over pretrained knowledge.
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 types implementing ConvertibleToPython auto-convert when passed to Python
functions. For explicit conversion: value.to_python_object().
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 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
# 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")
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'"
# 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)))
Mojo can build Python extension modules (.so files) via PythonModuleBuilder.
The pattern:
@export def PyInit_<module_name>() -> PythonObjectPythonModuleBuilder to register functions, types, and methodsmojo build --emit shared-libimport mojo.importer for auto-compilation)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 + "!")
@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))
| 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").
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
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.
# 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
development
Use after completing a bug fix, feature, refactor, or tk task when the first implementation taught enough context to replace it with a simpler, cleaner, or more coherent version before finalizing.
development
Use when writing, migrating, or reviewing Zig code across recent stable versions (0.14-0.16), especially to correct stale syntax or stdlib, build.zig, allocator, formatting, or runtime API knowledge.
documentation
Use when reviewing or revising text (prose, docs, commits) to remove AI patterns and improve voice/clarity.
content-media
Use when fetching X/Twitter post content by URL, or searching for recent X posts.