Introduction

RustPython is a Python interpreter written in Rust.

Getting Started

Installation

Requirements

RustPython requires Rust latest stable version to be installed

Stable

The latest stable version of the library can be installed using the following command:

cargo add rustpython

or by adding the following to your Cargo.toml:

[dependencies]
rustpython = "0.4"

Nightly

Nightly releases are built weekly and are released on git.

[dependencies]
rustpython = { git = "https://github.com/RustPython/RustPython", tag = "2025-02-24-main-13" }

The tag should be pointed to the latest tag found at https://github.com/RustPython/RustPython/tags.

Features

By default threading, stdlib, and importlib are enabled.

bz2

If you'd like to use the bz2 module, you can enable the bz2 feature.

stdlib

stdlib is the default feature that enables the standard library.

sqlite

If you'd like to use the sqlite3 module, you can enable the sqlite feature.

ssl

If you'd like to make https requests, you can enable the ssl feature, which also lets you install the pip package manager. Note that on Windows, you may need to install OpenSSL, or you can enable the ssl-vendor feature instead, which compiles OpenSSL for you but requires a C compiler, perl, and make. OpenSSL version 3 is expected and tested in CI. Older versions may not work.

Initial Setup

First rustpython_vm needs to be imported. If rustpython is installed, it can be imported as a re-export:

#![allow(unused)]
fn main() {
use rustpython::vm;
}

if rustpython_vm is installed, it can be imported just like any other module.

use rustpython::vm;

fn main() -> vm::PyResult<()> {
    vm::Interpreter::without_stdlib(Default::default()).enter(|vm| {
        let scope = vm.new_scope_with_builtins();
        let source = r#"print("Hello World!")"#;
        let code_obj = vm
            .compile(source, vm::compiler::Mode::Exec, "<embedded>".to_owned())
            .map_err(|err| vm.new_syntax_error(&err, Some(source)))?;

        vm.run_code_obj(code_obj, scope)?;

        Ok(())
    })
}

This will print Hello World! to the console.

Adding the standard library

If the stdlib feature is enabled, the standard library can be added to the interpreter by calling add_native_modules with the result of rustpython_stdlib::get_module_inits().

use rustpython::vm as vm;
use std::process::ExitCode;
use vm::{Interpreter, builtins::PyStrRef};

fn py_main(interp: &Interpreter) -> vm::PyResult<PyStrRef> {
    interp.enter(|vm| {
        let scope = vm.new_scope_with_builtins();
        let source = r#"print("Hello World!")"#;
        let code_obj = vm
            .compile(source, vm::compiler::Mode::Exec, "<embedded>".to_owned())
            .map_err(|err| vm.new_syntax_error(&err, Some(source)))?;

        vm.run_code_obj(code_obj, scope)?;
    })
}

fn main() -> ExitCode {
    // Add standard library path
    let mut settings = vm::Settings::default();
    settings.path_list.push("Lib".to_owned());
    let interp = vm::Interpreter::with_init(settings, |vm| {
        vm.add_native_modules(rustpython_stdlib::get_module_inits());
    });
    let result = py_main(&interp);
    let result = result.map(|result| {
        println!("name: {result}");
    });
    ExitCode::from(interp.run(|_vm| result))
}

to import a module, the following code can be used:

#![allow(unused)]
fn main() {
// Add local library path
vm.insert_sys_path(vm.new_pyobj("<module_path>"))
    .expect("add examples to sys.path failed");
let module = vm.import("<module_name>", 0)?;
}

Interpretation between Rust and Python

Calling Rust from Python

Structure

use rustpython::vm::pymodule;
#[pymodule]
mod test_module {
    #[pyattr]
    pub const THE_ANSWER: i32 = 42;

    #[pyfunction]
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    #[pyattr]
    #[pyclass]
    pub struct TestClass {
        pub value: i32,
    }

    #[pyclass]
    impl TestClass {
        #[pygetset]
        pub fn value(&self) -> i32 {
            self.value
        }

        #[pymethod]
        pub fn get_info(&self) -> i32 {
            self.value * 2
        }
    }
}

This code defines a Python module named test_module with two items: a constant named THE_ANSWER and a function named add. The #[pymodule] attribute is used to mark the module, and the #[pyattr] and #[pyfunction] attributes are used to mark the constant and function, respectively.

RustPython allows for 3 types of items in a module:

  • Variables: Defined using the #[pyattr] attribute.
  • Functions: Defined using the #[pyfunction] attribute.
  • Classes: Defined using the #[pyclass] attribute.

General Configuration

Most attributes have a name parameter that can be used to specify the name of the item in Python. If the name parameter is not provided, the Rust identifier is used as the name in Python.

Variables

Variables are defined using the #[pyattr] attribute. A variable can either be a constant or a function. Note that classes are treated as attributes in RustPython and are annotated with #[pyattr] as well, but that can be done away with if needed.

#![allow(unused)]
fn main() {
#[pyattr]
const THE_ANSWER: i32 = 42;
// ... or
#[pyattr]
fn the_answer() -> i32 {
    42
}
// ... or
// this will cache the result of the function
// and return the same value every time it is called
#[pyattr(once)]
fn cached_answer() -> i32 {
    42
}
}

Valid Arguments/Return Types

Every input and return value must be convertible to PyResult. This is defined as IntoPyResult trait. So any return value of them must implement IntoPyResult. It will be PyResult<PyObjectRef>, PyObjectRef and any PyResult<T> when T implements IntoPyObject. Practically we can list them like:

  • Any T when PyResult<T> is possible
  • PyObjectRef
  • PyResult<()> and () as None
  • PyRef<T: PyValue> like PyIntRef, PyStrRef
  • T: PyValue like PyInt, PyStr
  • Numbers like usize or f64 for PyInt and PyFloat
  • String for PyStr
  • And more types implementing IntoPyObject.

The vm paramter is optional. We add it as the last parameter unless we don't use vm at all - very rare case. It takes an object obj as PyObjectRef, which is a general python object. It returns PyResult<String>, which will turn into PyResult<PyObjectRef> the same representation of PyResult. The vm parameter does not need to be passed in by the python code.

If needed a seperate struct can be used for arguments using the FromArgs trait like so:

#![allow(unused)]
fn main() {
#[derive(FromArgs)]
struct BisectArgs {
    a: PyObjectRef,
    x: PyObjectRef
    #[pyarg(any, optional)]
    lo: OptionalArg<ArgIndex>,
    #[pyarg(any, optional)]
    hi: OptionalArg<ArgIndex>,
    #[pyarg(named, default)]
    key: Option<PyObjectRef>,
}

#[pyfunction]
fn bisect_left(
    BisectArgs { a, x, lo, hi, key }: BisectArgs,
    vm: &VirtualMachine,
) -> PyResult<usize> {
    // ...
}

// or ...

#[pyfunction]
fn bisect_left(
    args: BisectArgs,
    vm: &VirtualMachine,
) -> PyResult<usize> {
    // ...
}
}

Errors

Returning a PyResult is the supported error handling strategy. Builtin python errors are created with vm.new_xxx_error methods.

Custom Errors

#[pyattr(once)]
fn error(vm: &VirtualMachine) -> PyTypeRef {
    vm.ctx.new_exception_type(
        "<module_name>",
        "<error_name>",
        Some(vec![vm.ctx.exceptions.exception_type.to_owned()]),
    )
}

// convenience function
fn new_error(message: &str, vm: &VirtualMachine) -> PyBaseExceptionRef {
    vm.new_exception_msg(vm.class("<module_name>", "<error_name>"), message.to_owned())
}

Functions

Functions are defined using the #[pyfunction] attribute.

#![allow(unused)]
fn main() {
#[pyfunction]
fn add(a: i32, b: i32) -> i32 {
    a + b
}
}

Classes

Classes are defined using the #[pyclass] attribute.

#![allow(unused)]
fn main() {
#[pyclass]
pub struct TestClass {
    pub value: i32,
}
#[pyclass]
impl TestClass {
}
}

Associated Data

TODO.

Methods

TODO.

Getters and Setters

TODO.

Class Methods

TODO.

Static Methods

TODO.

Inheritance

TODO.

Calling Python from Rust

TODO.