Skip to main content

A Guide to MoonBit Python Integration

· 12 min read

Introduction

Python, with its concise syntax and vast ecosystem, has become one of the most popular programming languages today. However, discussions around its performance bottlenecks and the maintainability of its dynamic typing system in large-scale projects have never ceased. To address these challenges, the developer community has explored various optimization paths.

The python.mbt tool, officially launched by MoonBit, offers a new perspective. It allows developers to call Python code directly within the MoonBit environment. This combination aims to merge MoonBit's static type safety and high-performance potential with Python's mature ecosystem. Through python.mbt, developers can leverage MoonBit's static analysis capabilities, modern build and testing tools, while enjoying Python's rich library functions, making it possible to build large-scale, high-performance system-level software.

This article aims to delve into the working principles of python.mbt and provide a practical guide. It will answer common questions such as: How does python.mbt work? Is it slower than native Python due to an added intermediate layer? What are its advantages over existing tools like C++'s pybind11 or Rust's PyO3? To answer these questions, we first need to understand the basic workflow of the Python interpreter.

How the Python Interpreter Works

The Python interpreter executes code in three main stages:

  1. Parsing: This stage includes lexical analysis and syntax analysis. The interpreter breaks down human-readable Python source code into tokens and then organizes these tokens into a tree-like structure, the Abstract Syntax Tree (AST), based on syntax rules.

    For example, for the following Python code:

    def add(x, y):
      return x + y
    
    a = add(1, 2)
    print(a)
    

    We can use Python's ast module to view its generated AST structure:

    Module(
        body=[
            FunctionDef(
                name='add',
                args=arguments(
                    args=[
                        arg(arg='x'),
                        arg(arg='y')]),
                body=[
                    Return(
                        value=BinOp(
                            left=Name(id='x', ctx=Load()),
                            op=Add(),
                            right=Name(id='y', ctx=Load())))]),
            Assign(
                targets=[
                    Name(id='a', ctx=Store())],
                value=Call(
                    func=Name(id='add', ctx=Load()),
                    args=[
                        Constant(value=1),
                        Constant(value=2)])),
            Expr(
                value=Call(
                    func=Name(id='print', ctx=Load()),
                    args=[
                        Name(id='a', ctx=Load())]))])
    
  2. Compilation: Next, the Python interpreter compiles the AST into a lower-level, more linear intermediate representation called bytecode. This is a platform-independent instruction set designed for the Python Virtual Machine (PVM).

    Using Python's dis module, we can view the bytecode corresponding to the above code:

      2           LOAD_CONST               0 (<code object add>)
                  MAKE_FUNCTION
                  STORE_NAME               0 (add)
    
      5           LOAD_NAME                0 (add)
                  PUSH_NULL
                  LOAD_CONST               1 (1)
                  LOAD_CONST               2 (2)
                  CALL                     2
                  STORE_NAME               1 (a)
    
      6           LOAD_NAME                2 (print)
                  PUSH_NULL
                  LOAD_NAME                1 (a)
                  CALL                     1
                  POP_TOP
                  RETURN_CONST             3 (None)
    
  3. Execution: Finally, the Python Virtual Machine (PVM) executes the bytecode instructions one by one. Each instruction corresponds to a C function call in the CPython interpreter's underlying layer. For example, LOAD_NAME looks up a variable, and BINARY_OP performs a binary operation. It is this process of interpreting and executing instructions one by one that is the main source of Python's performance overhead. A simple 1 + 2 operation involves the entire complex process of parsing, compilation, and virtual machine execution.

Understanding this process helps us grasp the basic approaches to Python performance optimization and the design philosophy of python.mbt.

Paths to Optimizing Python Performance

Currently, there are two mainstream methods for improving Python program performance:

  1. Just-In-Time (JIT) Compilation: Projects like PyPy analyze a running program and compile frequently executed "hotspot" bytecode into highly optimized native machine code, thereby bypassing the PVM's interpretation and significantly speeding up computationally intensive tasks. However, JIT is not a silver bullet; it cannot solve the inherent problems of Python's dynamic typing, such as the difficulty of effective static analysis in large projects, which poses challenges for software maintenance.
  2. Native Extensions: Developers can use languages like C++ (with pybind11) or Rust (with PyO3) to directly call Python functions or to write performance-critical modules that are then called from Python. This method can achieve near-native performance, but it requires developers to be proficient in both Python and a complex system-level language, presenting a steep learning curve and a high barrier to entry for most Python programmers.

python.mbt is also a native extension. But compared to languages like C++ and Rust, it attempts to find a new balance between performance, ease of use, and engineering capabilities, with a greater emphasis on using Python features directly within the MoonBit language.

  1. High-Performance Core: MoonBit is a statically typed, compiled language whose code can be efficiently compiled into native machine code. Developers can implement computationally intensive logic in MoonBit to achieve high performance from the ground up.
  2. Seamless Python Calls: python.mbt interacts directly with CPython's C-API to call Python modules and functions. This means call overhead is minimized, bypassing Python's parsing and compilation stages and going straight to the virtual machine execution layer.
  3. Gentler Learning Curve: Compared to C++ and Rust, MoonBit's syntax is more modern and concise. It also has comprehensive support for functional programming, a documentation system, unit testing, and static analysis tools, making it more friendly to developers accustomed to Python.
  4. Improved Engineering and AI Collaboration: MoonBit's strong type system and clear interface definitions make code intent more explicit and easier for static analysis tools and AI-assisted programming tools to understand. This helps maintain code quality in large projects and improves the efficiency and accuracy of collaborative coding with AI.

Using Pre-wrapped Python Libraries in MoonBit

To facilitate developer use, MoonBit will officially wrap mainstream Python libraries once the build system and IDE are mature. After wrapping, users can use these Python libraries in their projects just like importing regular MoonBit packages. Let's take the matplotlib plotting library as an example.

First, add the matplotlib dependency in your project's root moon.pkg.json or via the terminal:

moon update
moon add Kaida-Amethyst/matplotlib

Then, declare the import in the moon.pkg.json of the sub-package where you want to use the library. Here, we follow Python's convention and set an alias plt:

{
  "import": [
    {
      "path": "Kaida-Amethyst/matplotlib",
      "alias": "plt"
    }
  ]
}

After configuration, you can call matplotlib in your MoonBit code to create plots:

let 
(Double) -> Double
sin
: (
Double
Double
) ->
Double
Double
=
(x : Double) -> Double

Calculates the sine of a number in radians. Handles special cases and edge conditions according to IEEE 754 standards.

Parameters:

  • x : The angle in radians for which to calculate the sine.

Returns the sine of the angle x.

Example:

inspect(@math.sin(0.0), content="0")
inspect(@math.sin(1.570796326794897), content="1") // pi / 2
inspect(@math.sin(2.0), content="0.9092974268256817")
inspect(@math.sin(-5.0), content="0.9589242746631385")
inspect(@math.sin(31415926535897.9323846), content="0.0012091232715481885")
inspect(@math.sin(@double.not_a_number), content="NaN")
inspect(@math.sin(@double.infinity), content="NaN")
inspect(@math.sin(@double.neg_infinity), content="NaN")
@math.sin
fn main { let
Array[Double]
x
=
type Array[T]

An Array is a collection of values that supports random access and can grow in size.

Array
::
(Int, (Int) -> Double) -> Array[Double]

Creates a new array of the specified length, where each element is initialized using an index-based initialization function.

Parameters:

  • length : The length of the new array. If length is less than or equal to 0, returns an empty array.
  • initializer : A function that takes an index (starting from 0) and returns a value of type T. This function is called for each index to initialize the corresponding element.

Returns a new array of type Array[T] with the specified length, where each element is initialized using the provided function.

Example:

  let arr = Array::makei(3, i => i * 2)
  inspect(arr, content="[0, 2, 4]")
makei
(100, fn(
Int
i
) {
Int
i
.
(self : Int) -> Double

Converts a 32-bit integer to a double-precision floating-point number. The conversion preserves the exact value since all integers in the range of Int can be represented exactly as Double values.

Parameters:

  • self : The 32-bit integer to be converted.

Returns a double-precision floating-point number that represents the same numerical value as the input integer.

Example:

  let n = 42
  inspect(n.to_double(), content="42")
  let neg = -42
  inspect(neg.to_double(), content="-42")
to_double
()
(self : Double, other : Double) -> Double

Multiplies two double-precision floating-point numbers. This is the implementation of the * operator for Double type.

Parameters:

  • self : The first double-precision floating-point operand.
  • other : The second double-precision floating-point operand.

Returns a new double-precision floating-point number representing the product of the two operands. Special cases follow IEEE 754 standard:

  • If either operand is NaN, returns NaN
  • If one operand is infinity and the other is zero, returns NaN
  • If one operand is infinity and the other is a non-zero finite number, returns infinity with the appropriate sign
  • If both operands are infinity, returns infinity with the appropriate sign

Example:

  inspect(2.5 * 2.0, content="5")
  inspect(-2.0 * 3.0, content="-6")
  let nan = 0.0 / 0.0 // NaN
  inspect(nan * 1.0, content="NaN")
*
0.1 })
let
Array[Double]
y
=
Array[Double]
x
.
(self : Array[Double], f : (Double) -> Double) -> Array[Double]

Maps a function over the elements of the array.

Example

  let v = [3, 4, 5]
  let v2 = v.map((x) => {x + 1})
  assert_eq(v2, [4, 5, 6])
map
(
(Double) -> Double
sin
)
// To ensure type safety, the wrapped subplots interface always returns a tuple of a fixed type. // This avoids the dynamic behavior in Python where the return type depends on the arguments. let (_,
Unit
axes
) =
(Int, Int) -> (Unit, Unit)
plt::subplots
(1, 1)
// Use the .. cascade call syntax
Unit
axes
[0
(Int) -> Unit
]
[0]
..
(Array[Double], Array[Double], Unit, Unit, Int) -> Unit
plot
(
Array[Double]
x
,
Array[Double]
y
,
Unit
color
=
Unit
Green
,
Unit
linestyle
=
Unit
Dashed
,
Int
linewidth
= 2)
..
(String) -> Unit
set_title
("Sine of x")
..
(String) -> Unit
set_xlabel
("x")
..
(String) -> Unit
set_ylabel
("sin(x)")
() -> Unit
@plt.show
()
}

Currently, on macOS and Linux, MoonBit's build system can automatically handle dependencies. On Windows, users may need to manually install a C compiler and configure the Python environment. Future MoonBit IDEs will aim to simplify this process.

Using Unwrapped Python Modules in MoonBit

The Python ecosystem is vast, and even with AI technology, relying solely on official wrappers is not realistic. Fortunately, we can use the core features of python.mbt to interact directly with any Python module. Below, we demonstrate this process using the simple time module from the Python standard library.

Introducing python.mbt

First, ensure your MoonBit toolchain is up to date, then add the python.mbt dependency:

moon update
moon add Kaida-Amethyst/python

Next, import it in your package's moon.pkg.json:

{
  "import": ["Kaida-Amethyst/python"]
}

python.mbt automatically handles the initialization (Py_Initialize) and shutdown of the Python interpreter, so developers don't need to manage it manually.

Importing Python Modules

Use the @python.pyimport function to import modules. To avoid performance loss from repeated imports, it is recommended to use a closure technique to cache the imported module object:

// Define a struct to hold the Python module object for enhanced type safety
pub struct TimeModule {
  
?
time_mod
: PyModule
} // Define a function that returns a closure for getting a TimeModule instance fn
() -> () -> TimeModule
import_time_mod
() -> () ->
struct TimeModule {
  time_mod: ?
}
TimeModule
{
// The import operation is performed only on the first call guard
(String) -> Unit
@python.pyimport
("time") is
(?) -> Unit
Some
(
?
time_mod
) else {
(input : String) -> Unit

Prints any value that implements the Show trait to the standard output, followed by a newline.

Parameters:

  • value : The value to be printed. Must implement the Show trait.

Example:

  println(42)
  println("Hello, World!")
  println([1, 2, 3])
println
("Failed to load Python module: time")
() -> () -> TimeModule
panic
("ModuleLoadError")
} let
TimeModule
time_mod
=
struct TimeModule {
  time_mod: ?
}
TimeModule
::{
?
time_mod
}
// The returned closure captures the time_mod variable fn () {
TimeModule
time_mod
}
} // Create a global time_mod "getter" function let
() -> TimeModule
time_mod
: () ->
struct TimeModule {
  time_mod: ?
}
TimeModule
=
() -> () -> TimeModule
import_time_mod
()

In subsequent code, we should always call time_mod() to get the module, not import_time_mod.

Converting Between MoonBit and Python Objects

To call Python functions, we need to convert between MoonBit objects and Python objects (PyObject).

  1. Integers: Use PyInteger::from to create a PyInteger from an Int64, and to_int64() for the reverse conversion.

    test "py_integer_conversion" {
      let 
    Int64
    n
    :
    Int64
    Int64
    = 42
    let
    &Show
    py_int
    =
    (Int64) -> &Show
    PyInteger::from
    (
    Int64
    n
    )
    (obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

    Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

    Parameters:

    • object : The object to be inspected. Must implement the Show trait.
    • content : The expected string representation of the object. Defaults to an empty string.
    • location : Source code location information for error reporting. Automatically provided by the compiler.
    • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

    Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

    Example:

      inspect(42, content="42")
      inspect("hello", content="hello")
      inspect([1, 2, 3], content="[1, 2, 3]")
    inspect
    (
    &Show
    py_int
    ,
    String
    content
    ="42")
    (a : Int64, b : Int64, msg? : String, loc~ : SourceLoc = _) -> Unit raise Error

    Asserts that two values are equal. If they are not equal, raises a failure with a message containing the source location and the values being compared.

    Parameters:

    • a : First value to compare.
    • b : Second value to compare.
    • loc : Source location information to include in failure messages. This is usually automatically provided by the compiler.

    Throws a Failure error if the values are not equal, with a message showing the location of the failing assertion and the actual values that were compared.

    Example:

      assert_eq(1, 1)
      assert_eq("hello", "hello")
    assert_eq
    (
    &Show
    py_int
    .
    () -> Int64
    to_int64
    (), 42L)
    }
  2. Floats: Use PyFloat::from and to_double.

    test "py_float_conversion" {
      let 
    Double
    n
    :
    Double
    Double
    = 3.5
    let
    &Show
    py_float
    =
    (Double) -> &Show
    PyFloat::from
    (
    Double
    n
    )
    (obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

    Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

    Parameters:

    • object : The object to be inspected. Must implement the Show trait.
    • content : The expected string representation of the object. Defaults to an empty string.
    • location : Source code location information for error reporting. Automatically provided by the compiler.
    • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

    Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

    Example:

      inspect(42, content="42")
      inspect("hello", content="hello")
      inspect([1, 2, 3], content="[1, 2, 3]")
    inspect
    (
    &Show
    py_float
    ,
    String
    content
    ="3.5")
    (a : Double, b : Double, msg? : String, loc~ : SourceLoc = _) -> Unit raise Error

    Asserts that two values are equal. If they are not equal, raises a failure with a message containing the source location and the values being compared.

    Parameters:

    • a : First value to compare.
    • b : Second value to compare.
    • loc : Source location information to include in failure messages. This is usually automatically provided by the compiler.

    Throws a Failure error if the values are not equal, with a message showing the location of the failing assertion and the actual values that were compared.

    Example:

      assert_eq(1, 1)
      assert_eq("hello", "hello")
    assert_eq
    (
    &Show
    py_float
    .
    () -> Double
    to_double
    (), 3.5)
    }
  3. Strings: Use PyString::from and to_string.

    test "py_string_conversion" {
      let 
    &Show
    py_str
    =
    (String) -> &Show
    PyString::from
    ("hello")
    (obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

    Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

    Parameters:

    • object : The object to be inspected. Must implement the Show trait.
    • content : The expected string representation of the object. Defaults to an empty string.
    • location : Source code location information for error reporting. Automatically provided by the compiler.
    • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

    Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

    Example:

      inspect(42, content="42")
      inspect("hello", content="hello")
      inspect([1, 2, 3], content="[1, 2, 3]")
    inspect
    (
    &Show
    py_str
    ,
    String
    content
    ="'hello'")
    (a : String, b : String, msg? : String, loc~ : SourceLoc = _) -> Unit raise Error

    Asserts that two values are equal. If they are not equal, raises a failure with a message containing the source location and the values being compared.

    Parameters:

    • a : First value to compare.
    • b : Second value to compare.
    • loc : Source location information to include in failure messages. This is usually automatically provided by the compiler.

    Throws a Failure error if the values are not equal, with a message showing the location of the failing assertion and the actual values that were compared.

    Example:

      assert_eq(1, 1)
      assert_eq("hello", "hello")
    assert_eq
    (
    &Show
    py_str
    .
    (&Show) -> String
    to_string
    (), "hello")
    }
  4. Lists: You can create an empty PyList and append elements, or create one directly from an Array[&IsPyObject].

    test "py_list_from_array" {
      let 
    Unit
    one
    =
    (Int) -> Unit
    PyInteger::from
    (1)
    let
    Unit
    two
    =
    (Double) -> Unit
    PyFloat::from
    (2.0)
    let
    Unit
    three
    =
    (String) -> Unit
    PyString::from
    ("three")
    let
    Array[Unit]
    arr
    Array[Unit]
    :
    type Array[T]

    An Array is a collection of values that supports random access and can grow in size.

    Array
    Array[Unit]
    [&IsPyObject]
    = [
    Unit
    one
    ,
    Unit
    two
    ,
    Unit
    three
    ]
    let
    &Show
    list
    =
    (Array[Unit]) -> &Show
    PyList::from
    (
    Array[Unit]
    arr
    )
    (obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

    Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

    Parameters:

    • object : The object to be inspected. Must implement the Show trait.
    • content : The expected string representation of the object. Defaults to an empty string.
    • location : Source code location information for error reporting. Automatically provided by the compiler.
    • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

    Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

    Example:

      inspect(42, content="42")
      inspect("hello", content="hello")
      inspect([1, 2, 3], content="[1, 2, 3]")
    inspect
    (
    &Show
    list
    ,
    String
    content
    ="[1, 2.0, 'three']")
    }
  5. Tuples: PyTuple requires specifying the size first, then filling elements one by one using the set method.

    test "py_tuple_creation" {
      let 
    &Show
    tuple
    =
    (Int) -> &Show
    PyTuple::new
    (3)
    &Show
    tuple
    ..
    (Int, Unit) -> Unit
    set
    (0,
    (Int) -> Unit
    PyInteger::from
    (1))
    ..
    (Int, Unit) -> Unit
    set
    (1,
    (Double) -> Unit
    PyFloat::from
    (2.0))
    ..
    (Int, Unit) -> Unit
    set
    (2,
    (String) -> Unit
    PyString::from
    ("three"))
    (obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

    Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

    Parameters:

    • object : The object to be inspected. Must implement the Show trait.
    • content : The expected string representation of the object. Defaults to an empty string.
    • location : Source code location information for error reporting. Automatically provided by the compiler.
    • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

    Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

    Example:

      inspect(42, content="42")
      inspect("hello", content="hello")
      inspect([1, 2, 3], content="[1, 2, 3]")
    inspect
    (
    &Show
    tuple
    ,
    String
    content
    ="(1, 2.0, 'three')")
    }
  6. Dictionaries: PyDict mainly supports strings as keys. Use new to create a dictionary and set to add key-value pairs. For non-string keys, use set_by_obj.

    test "py_dict_creation" {
      let 
    &Show
    dict
    =
    () -> &Show
    PyDict::new
    ()
    &Show
    dict
    ..
    (String, Unit) -> Unit
    set
    ("one",
    (Int) -> Unit
    PyInteger::from
    (1))
    ..
    (String, Unit) -> Unit
    set
    ("two",
    (Double) -> Unit
    PyFloat::from
    (2.0))
    (obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

    Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

    Parameters:

    • object : The object to be inspected. Must implement the Show trait.
    • content : The expected string representation of the object. Defaults to an empty string.
    • location : Source code location information for error reporting. Automatically provided by the compiler.
    • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

    Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

    Example:

      inspect(42, content="42")
      inspect("hello", content="hello")
      inspect([1, 2, 3], content="[1, 2, 3]")
    inspect
    (
    &Show
    dict
    ,
    String
    content
    ="{'one': 1, 'two': 2.0}")
    }

When getting elements from Python composite types, python.mbt performs runtime type checking and returns an Optional[PyObjectEnum] to ensure type safety.

test "py_list_get" {
  let 
Unit
list
=
() -> Unit
PyList::new
()
Unit
list
.
(Unit) -> Unit
append
(
(Int) -> Unit
PyInteger::from
(1))
Unit
list
.
(Unit) -> Unit
append
(
(String) -> Unit
PyString::from
("hello"))
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
Unit
list
.
(Int) -> Unit
get
(0).
() -> &Show
unwrap
(),
String
content
="PyInteger(1)")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
Unit
list
.
(Int) -> Unit
get
(1).
() -> &Show
unwrap
(),
String
content
="PyString('hello')")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
Unit
list
.
(Int) -> &Show
get
(2),
String
content
="None") // Index out of bounds returns None
}

Calling Functions in a Module

Calling a function is a two-step process: first, get the function object with get_attr, then execute the call with invoke. The return value of invoke is a PyObject that requires pattern matching and type conversion.

Here is the MoonBit wrapper for time.sleep and time.time:

// Wrap time.sleep
pub fn 
(seconds : Double) -> Unit
sleep
(
Double
seconds
:
Double
Double
) ->
Unit
Unit
{
let
TimeModule
lib
=
() -> TimeModule
time_mod
()
guard
TimeModule
lib
.
?
time_mod
.
(String) -> Unit
get_attr
("sleep") is
(_/0) -> Unit
Some
(
(Unit) -> _/0
PyCallable
(
Unit
f
)) else {
(input : String) -> Unit

Prints any value that implements the Show trait to the standard output, followed by a newline.

Parameters:

  • value : The value to be printed. Must implement the Show trait.

Example:

  println(42)
  println("Hello, World!")
  println([1, 2, 3])
println
("get function `sleep` failed!")
() -> Unit
panic
()
} let
Unit
args
=
(Int) -> Unit
PyTuple::new
(1)
Unit
args
.
(Int, Unit) -> Unit
set
(0,
(Double) -> Unit
PyFloat::from
(
Double
seconds
))
match (try?
Unit
f
.
(Unit) -> Unit
invoke
(
Unit
args
)) {
Result[Unit, Error]
Ok
(_) =>
Unit
Ok
(())
(Error) -> Result[Unit, Error]
Err
(
Error
e
) => {
(input : String) -> Unit

Prints any value that implements the Show trait to the standard output, followed by a newline.

Parameters:

  • value : The value to be printed. Must implement the Show trait.

Example:

  println(42)
  println("Hello, World!")
  println([1, 2, 3])
println
("invoke `sleep` failed!")
() -> Unit
panic
()
} } } // Wrap time.time pub fn
() -> Double
time
() ->
Double
Double
{
let
TimeModule
lib
=
() -> TimeModule
time_mod
()
guard
TimeModule
lib
.
?
time_mod
.
(String) -> Unit
get_attr
("time") is
(_/0) -> Unit
Some
(
(Unit) -> _/0
PyCallable
(
Unit
f
)) else {
(input : String) -> Unit

Prints any value that implements the Show trait to the standard output, followed by a newline.

Parameters:

  • value : The value to be printed. Must implement the Show trait.

Example:

  println(42)
  println("Hello, World!")
  println([1, 2, 3])
println
("get function `time` failed!")
() -> Double
panic
()
} match (try?
Unit
f
.
() -> Unit
invoke
()) {
(Unit) -> Result[Unit, Error]
Ok
(
(_/0) -> Unit
Some
(
(Unit) -> _/0
PyFloat
(
Unit
t
))) =>
Unit
t
.
() -> Double
to_double
()
_ => {
(input : String) -> Unit

Prints any value that implements the Show trait to the standard output, followed by a newline.

Parameters:

  • value : The value to be printed. Must implement the Show trait.

Example:

  println(42)
  println("Hello, World!")
  println([1, 2, 3])
println
("invoke `time` failed!")
() -> Double
panic
()
} } }

After wrapping, we can use them in a type-safe way in MoonBit:

test "sleep" {
  let 
Unit
start
=
() -> Double
time
().
() -> Unit
unwrap
()
(seconds : Double) -> Unit
sleep
(1)
let
Unit
end
=
() -> Double
time
().
() -> Unit
unwrap
()
(input : String) -> Unit

Prints any value that implements the Show trait to the standard output, followed by a newline.

Parameters:

  • value : The value to be printed. Must implement the Show trait.

Example:

  println(42)
  println("Hello, World!")
  println([1, 2, 3])
println
("start = \{
Unit
start
}")
(input : String) -> Unit

Prints any value that implements the Show trait to the standard output, followed by a newline.

Parameters:

  • value : The value to be printed. Must implement the Show trait.

Example:

  println(42)
  println("Hello, World!")
  println([1, 2, 3])
println
("end = \{
Unit
end
}")
}

Practical Advice

  1. Define Clear Boundaries: Treat python.mbt as the "glue layer" connecting MoonBit and the Python ecosystem. Keep core computation and business logic in MoonBit to leverage its performance and type system advantages, and only use python.mbt when necessary to call Python-exclusive libraries.

  2. Use ADTs Instead of String Magic: Many Python functions accept specific strings as arguments to control behavior. In MoonBit wrappers, these "magic strings" should be converted to Algebraic Data Types (ADTs), i.e., enums. This leverages MoonBit's type system to move runtime value checks to compile time, greatly enhancing code robustness.

  3. Thorough Error Handling: The examples in this article use panic or return simple strings for brevity. In production code, you should define dedicated error types and pass and handle them through the Result type, providing clear error context.

  4. Map Keyword Arguments: Python functions extensively use keyword arguments (kwargs), such as plot(color='blue', linewidth=2). This can be elegantly mapped to MoonBit's Labeled Arguments. When wrapping, prioritize using labeled arguments to provide a similar development experience.

    For example, a Python function that accepts kwargs:

    # graphics.py
    def draw_line(points, color="black", width=1):
        # ... drawing logic ...
        print(f"Drawing line with color {color} and width {width}")
    

    Its MoonBit wrapper can be designed as:

    fn draw_line(points: Array[Point], color~: Color = Black, width: Int = 1) -> Unit {
      let points : PyList = ... // convert Array[Point] to PyList
    
      // construct args
      let args = PyTuple::new(1)
      args .. set(0, points)
    
      // construct kwargs
      let kwargs = PyDict::new()
      kwargs
      ..set("color", PyString::from(color))
      ...set("width", PyInteger::from(width))
      match (try? f.invoke(args~, kwargs~)) {
        Ok(_) => ()
        _ => {
          // handle error
        }
      }
    }
    
  5. Beware of Dynamism: Always remember that Python is dynamically typed. Any data obtained from Python should be treated as "untrusted" and must undergo strict type checking and validation. Avoid using unwrap as much as possible; instead, use pattern matching to safely handle all possible cases.

Conclusion

This article has outlined the working principles of python.mbt and demonstrated how to use it to call Python code in MoonBit, whether through pre-wrapped libraries or by interacting directly with Python modules. python.mbt is not just a tool; it represents a fusion philosophy: combining MoonBit's static analysis, high performance, and engineering advantages with Python's vast and mature ecosystem. We hope this article provides developers in the MoonBit and Python communities with a new, more powerful option for building future software.