con un clic
python-interop
// Nim to Python interoperability including nimpy for calling Python from Nim and exporting Nim to Python, nimporter for packaging Nim modules as Python packages, and cffi/ctypes for calling Nim from Python
// Nim to Python interoperability including nimpy for calling Python from Nim and exporting Nim to Python, nimporter for packaging Nim modules as Python packages, and cffi/ctypes for calling Nim from Python
Nim testing conventions, unittest framework, and C++ compatibility patterns
Nim bindings to libtorch for tensor operations with high-level sugar
Nim type system patterns and pitfalls
Common import patterns and pitfalls for the Tattletale Nim project
Regex functionality in Nim including std/re, std/nre wrappers around PCRE, and the pure Nim nim-regex alternative with linear-time matching guarantees
Nim's hash table module for key-value storage
| name | python-interop |
| description | Nim to Python interoperability including nimpy for calling Python from Nim and exporting Nim to Python, nimporter for packaging Nim modules as Python packages, and cffi/ctypes for calling Nim from Python |
This skill covers the main approaches for integrating Nim with Python: nimpy (call Python from Nim and export Nim to Python), nimporter (package Nim as Python modules), and cffi/ctypes (call Nim from Python).
There are two primary integration directions:
{.exportpy.} to expose Nim functions to Python, or use nimporter for packagingCommon use cases:
nimpy allows calling Python code and libraries directly from Nim. It provides ABI compatibility - compiled modules don't depend on a particular Python version.
import nimpy
let os = pyImport("os")
echo "Current dir is: ", os.getcwd().to(string)
# sum(range(1, 5))
let py = pyBuiltinsModule()
let s = py.sum(py.range(0, 5)).to(int)
assert s == 10
.to(T) APIlet np = pyImport("numpy")
let arr = np.array(@[1.0, 2.0, 3.0].toNdArray)
discard py.print(arr)
nimpy can export Nim functions as a Python module. This is how you create Python extensions in Nim.
# mymodule.nim - filename MUST match the module name
import nimpy
proc greet*(name: string): string {.exportpy.} =
return "Hello, " & name & "!"
# Windows:
nim c --app:lib --out:mymodule.pyd --threads:on --tlsEmulation:off --passL:-static mymodule
# Linux/macOS:
nim c --app:lib --out:mymodule.so --threads:on mymodule
# test.py
import mymodule
assert mymodule.greet("world") == "Hello, world!"
assert mymodule.greet(name="world") == "Hello, world!"
nimpy supports returning Nim complex types that Python can use:
import nimpy, tables, json
proc getTable*(): Table[string, int] {.exportpy.} =
result = {"Hello": 0, "SomeKey": 10}.toTable
proc getJsonAsDict*(): JsonNode {.exportpy.} =
result = %*{
"SomeKey": 1.0,
"Another": 5,
"Foo": [1, 2, 3.5, {"InArray": 5}],
"Bar": {"Nested": "Value"}
}
import mymodule
# Table becomes dict
table = mymodule.getTable()
assert table["Hello"] == 0
assert table["SomeKey"] == 10
# JsonNode becomes dict
json_obj = mymodule.getJsonAsDict()
assert json_obj["SomeKey"] == 1.0
assert json_obj["Foo"] == [1, 2, 3.5, {"InArray": 5}]
| Nim Type | Python Type |
|---|---|
| int | int |
| float | float |
| string | str |
| seq[T] | list |
| tuple | tuple |
| bool | bool |
| Table[K, V] | dict |
| JsonNode | dict |
| ref object of PyNimObjectExperimental | Python class |
nimpy can export Nim types as Python classes. This is useful for creating Python-native objects that wrap Nim state and behavior.
ref object that inherits from PyNimObjectExperimental (directly or indirectly)self as first argument to trigger type exportself as first argument become methods on the Python type# mymodule.nim
import nimpy
type
TestType* = ref object of PyNimObjectExperimental
myField*: string
proc setMyField*(self: TestType, value: string) {.exportpy.} =
self.myField = value
proc getMyField*(self: TestType): string {.exportpy.} =
self.myField
proc newTestType*(): TestType {.exportpy.} =
TestType()
# test.py
import mymodule
tt = mymodule.newTestType()
tt.setMyField("Hello")
assert tt.getMyField() == "Hello"
__init__ and __repr__nimpy provides special handling for initialization and representation:
__init__ - Export a proc as Python __init__ method when:
init##TypeNameself##TypeName__repr__ - Export a proc as Python __repr__ method when:
$self##TypeNamestringDocumentation - Set module and type docstrings:
setModuleDocString("This is a test module")
setDocStringForType(MyType, "This is a test type")
# simple.nim
import nimpy
import strformat
pyExportModule("simple") # Only needed if filename differs from module name
type
SimpleObj* = ref object of PyNimObjectExperimental
a*: int
## Export as __init__ (tp_init)
proc initSimpleObj*(self: SimpleObj, a: int = 1) {.exportpy.} =
echo "Calling initSimpleObj for SimpleObj"
self.a = a
## Export as __repr__ (tp_repr)
proc `$`*(self: SimpleObj): string {.exportpy.} =
&"SimpleObj(a={self.a})"
setModuleDocString("This is a test module")
setDocStringForType(SimpleObj, "This is a test type")
Compile:
nim c --app:lib -o:./simple.so ./simple.nim
Use in Python:
import simple
print(simple.__doc__) # This is a test module
print(simple.SimpleObj.__doc__) # This is a test type
obj = simple.SimpleObj(a=2)
print(obj) # SimpleObj(a=2)
# texport_pytype.nim
import nimpy
import nimpy/py_types
import nimpy/py_lib as lib
import strformat
type
PyCustomType* = ref object of PyNimObjectExperimental
a*: int
b*: float
c*: string
proc initPyCustomType*(self: PyCustomType, aa: int = 1, bb: float = 2.0, cc: string = "default") {.exportpy} =
self.a = aa
self.b = bb
self.c = cc
proc destroyPyCustomType*(self: PyCustomType) {.exportpy} =
discard # Cleanup if needed
proc `$`*(self: PyCustomType): string {.exportpy} =
&"a: {self.a}, b: {self.b}, c: {self.c}"
proc get_a*(self: PyCustomType): int {.exportpy} =
self.a
proc set_a*(self: PyCustomType, val: int) {.exportpy} =
self.a = val
setModuleDocString("Test module for exported Python types")
setDocStringForType(PyCustomType, "Custom type with a, b, c fields")
import unittest
suite "Test Exported Python Types":
let m = pyImport("texport_pytype")
test "Test __doc__":
check getAttr(m, "__doc__").`$` == "Test module for exported Python types"
check getAttr(getAttr(m, "PyCustomType"), "__doc__").`$` == "Custom type with a, b, c fields"
test "Test __init__ and methods":
let constructor = getAttr(m, "PyCustomType")
let obj = callObject(constructor, 99, 3.14, "hello")
check obj.get_a().to(int) == 99
obj.set_a(42)
check obj.get_a().to(int) == 42
test "Test __repr__":
let constructor = getAttr(m, "PyCustomType")
let obj = callObject(constructor, 99, 3.14, "hello")
check obj.`$` == "a: 99, b: 3.14, c: hello"
| Usage | Proc Signature | Becomes |
|---|---|---|
| Constructor | proc newType*(): Type | Class method (factory) |
__init__ | proc initType*(self: Type, args...) | Type.__init__(self, args...) |
__repr__ | proc $*(self: Type): string | Type.__repr__(self) |
| Method | proc method*(self: Type, args...): R | Type.method(self, args...) |
| Nim Type | Python Type |
|---|---|
| int | int |
| float | float |
| string | str |
| seq[T] | list |
| tuple | tuple |
| bool | bool |
| ref object of PyNimObjectExperimental | Python class |
Note: For Python class exports, the Nim type must inherit from PyNimObjectExperimental. See "Exporting Nim Types as Python Classes" above.
nimporter builds on nimpy to provide seamless import and packaging for distribution.
pip install nimporter
# main.py
import nimporter # Must import before Nim modules
import mymodule # Compiled automatically!
result = mymodule.greet("world")
Extension Modules (simple, direct import):
.nim fileExtension Libraries (full project):
libname.nim, libname.nim.cfg, libname.nimblemylibrary/
mylibrary.nim # Must be present
mylibrary.nim.cfg # Must be present (can be empty)
mylibrary.nimble # Must contain `requires "nimpy"`
# setup.py
import setuptools
from nimporter import get_nim_extensions, WINDOWS, MACOS, LINUX
setuptools.setup(
name='mylibrary',
install_requires=['nimporter'],
py_modules=['mylibrary.py'],
ext_modules=get_nim_extensions(platforms=[WINDOWS, LINUX, MACOS])
)
# Source distribution (end users need Nim compiler)
python setup.py sdist
# Binary distribution
python setup.py bdist_wheel
nimporter clean # Remove cached builds
nimporter compile # Precompile all extensions
nimporter list # List detected extensions
# Precompile for Docker (no Nim needed in container)
nimporter compile
Ensure __pycache__ directories are included in Docker image.
cffi provides a Python interface to call compiled Nim shared libraries.
# called.nim
proc nim_add*(num1: int, num2: int): int {.exportc.} =
return num1 + num2
Compile as a shared library:
nim c --app:lib called.nim
# Creates libcalled.so (or .pyd on Windows, .dylib on macOS)
from cffi import FFI
ffi = FFI()
ffi.cdef("""
int nim_add(int num1, int num2);
""")
lib = ffi.dlopen("./libcalled.so")
result = lib.nim_add(5, 10)
print(result) # 15
| Nim Type | C Type |
|---|---|
| int | long (c_long) |
| int8 | int8_t |
| int16 | int16_t |
| int32 | int32_t |
| int64 | int64_t |
| uint | unsigned long |
| float | double |
| cstring | char* |
| ptr T | T* |
ctypes is Python's built-in FFI library. Similar to cffi but uses stdlib only.
# partitions.nim
proc partitions*(cards: var array[0..9, int], subtotal: int): int {. exportc, dynlib .} =
var total: int
result = 0
for i in 0..9:
if cards[i] > 0:
total = subtotal + i + 1
if total < 21:
result += 1
cards[i] -= 1
result += partitions(cards, total)
cards[i] += 1
elif total == 21:
result += 1
return result
Compile:
nim c --app:lib --dynlib:yes partitions.nim
#!/usr/bin/env python
from ctypes import *
import os
lib = cdll.LoadLibrary(os.path.abspath("libpartitions.so"))
lib.partitions.argtypes = [POINTER(c_long), c_long]
lib.partitions.restype = c_long
deck = [4] * 9
deck.append(16)
for i in range(10):
deck[i] -= 1
p = 0
for j in range(10):
deck[j] -= 1
nums_arr = (c_long * len(deck))(*deck)
n = lib.partitions(nums_arr, c_long(j + 1))
deck[j] += 1
p += n
print(f'Dealer showing {i} partitions = {p}')
deck[i] += 1
| Feature | nimpy | nimporter | cffi | ctypes |
|---|---|---|---|---|
| Direction | Both | Nim→Python | Python→Nim | Python→Nim |
| Dependencies | nimpy | nimporter | cffi | stdlib |
| Ease of Use | Medium | Easy | Medium | Medium |
| Performance | Native | Native | Native | Native |
| Distribution | Manual | Wheels/Source | Source | Source |
| Type Safety | Nim | Nim | Manual | Manual |
| ABI Stable | Yes | Yes | N/A | N/A |
# Identify hot path in Python
def slow_function():
for i in range(1000000):
# compute-intensive work
# Rewrite in Nim with {.exportpy.}, package with nimporter
import nimpy
import arraymancer
import scinim/numpyarrays # For efficient numpy interop
let np = pyImport("numpy")
let scipy = pyImport("scipy")
# Use numpy/scipy directly
let result = scipy.special.gamma(nim_array.toNdArray)
import nimpy
proc calculate*(x: float): float {.exportpy.} =
result = x * 2.0
If you get dynamic module does not define module export function:
pip install find_libpython
python3 -c 'import find_libpython; print(find_libpython.find_libpython())'
Then set nimpy.py_lib.pyInitLibPath.
For multiple nimpy modules, consider moving Nim runtime to a separate shared library. See Nim docs on DLL generation.
Use --tlsEmulation:off and link statically with --passL:-static on Windows.
# Nim compiler
# https://nim-lang.org/install.html
# Python packages
pip install nimporter # For packaging Nim as Python modules
pip install cffi # For cffi approach (optional, ctypes is stdlib)
pip install find_libpython # For debugging libpython issues
# Nim packages
nimble install nimpy # For calling Python from Nim and exporting to Python