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
whenPyResult<T>
is possible PyObjectRef
PyResult<()>
and()
asNone
PyRef<T: PyValue>
likePyIntRef
,PyStrRef
T: PyValue
likePyInt
,PyStr
- Numbers like
usize
orf64
forPyInt
andPyFloat
String
forPyStr
- 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.