├── requirements.txt ├── img └── pyo3.png ├── multiply ├── multiply.py ├── src │ └── lib.rs └── Cargo.toml ├── pyo3 ├── fib.py ├── Cargo.toml ├── example.py └── src │ └── lib.rs ├── Dockerfile ├── .gitignore ├── LICENSE └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | maturin 2 | pydantic -------------------------------------------------------------------------------- /img/pyo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidvandeklundert/pyo3/HEAD/img/pyo3.png -------------------------------------------------------------------------------- /multiply/multiply.py: -------------------------------------------------------------------------------- 1 | import rust 2 | 3 | result = rust.multiply(2, 3) 4 | print(result) -------------------------------------------------------------------------------- /multiply/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | 3 | #[pyfunction] 4 | fn multiply(a: isize, b: isize) -> PyResult { 5 | Ok(a * b) 6 | } 7 | 8 | #[pymodule] 9 | fn rust(_py: Python, m: &PyModule) -> PyResult<()> { 10 | m.add_function(wrap_pyfunction!(multiply, m)?)?; 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /pyo3/fib.py: -------------------------------------------------------------------------------- 1 | def get_fibonacci(number: int) -> int: 2 | """Get the nth Fibonacci number.""" 3 | if number == 1: 4 | return 1 5 | elif number == 2: 6 | return 2 7 | 8 | total = 0 9 | last = 0 10 | current = 1 11 | for _ in range(1, number): 12 | total = last + current 13 | last = current 14 | current = total 15 | return total 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest 2 | RUN apt-get update 3 | RUN apt-get install -y python3.9 4 | RUN apt-get install -y python3-pip 5 | RUN apt-get install -y python3-venv 6 | RUN cd /opt && git clone https://github.com/saidvandeklundert/pyo3.git 7 | RUN cd /opt/pyo3/pyo3 && cargo update 8 | RUN python3 -m venv /opt/venv 9 | RUN . /opt/venv/bin/activate && pip install -r /opt/pyo3/requirements.txt 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | *.exe 5 | *.so 6 | # Generated by Cargo 7 | # will have compiled files and executables 8 | /target/ 9 | target/ 10 | 11 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 12 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 13 | Cargo.lock 14 | 15 | # These are backup files generated by rustfmt 16 | **/*.rs.bk 17 | -------------------------------------------------------------------------------- /multiply/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multiply" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | name = "rust" 8 | # "cdylib" is necessary to produce a shared library for Python to import from. 9 | # 10 | # Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able 11 | # to `use string_sum;` unless the "rlib" or "lib" crate type is also included, e.g.: 12 | # crate-type = ["cdylib", "rlib"] 13 | crate-type = ["cdylib"] 14 | 15 | [dependencies.pyo3] 16 | version = "0.15.0" 17 | features = ["extension-module"] 18 | 19 | -------------------------------------------------------------------------------- /pyo3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyo3_samples" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | name = "rust" 8 | # "cdylib" is necessary to produce a shared library for Python to import from. 9 | # 10 | # Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able 11 | # to `use string_sum;` unless the "rlib" or "lib" crate type is also included, e.g.: 12 | # crate-type = ["cdylib", "rlib"] 13 | crate-type = ["cdylib"] 14 | 15 | [dependencies.pyo3] 16 | version = "0.15.0" 17 | features = ["extension-module"] 18 | 19 | [dependencies] 20 | serde = { version = "1", features = ["derive"] } 21 | serde_json = "1" 22 | pyo3-log = "0.5.0" 23 | log = "0.4.14" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 saidvandeklundert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PyO3 examples 2 | 3 | This repo accompanies [this](https://saidvandeklundert.net/learn/2021-11-18-calling-rust-from-python-using-pyo3/) blog post. 4 | 5 | The Rust examples, found [here](https://github.com/saidvandeklundert/pyo3/blob/main/pyo3/src/lib.rs), include: 6 | - calculate the n-th Fibonacci in Python as well as in Rust 7 | - having Python use a variety of types in Rust functions 8 | - using a Rust struct in Python code 9 | - using Python to send JSON to Rust and serialize that JSON as a struct 10 | - allow Rust to use the logger from the Python runtime 11 | - generating an Error in Rust and catching it as an exception in Python 12 | 13 | The Rust code is being called from a Python script found [here](https://github.com/saidvandeklundert/pyo3/blob/main/pyo3/example.py). 14 | 15 | ## To play with the code: 16 | 17 |

18 | 19 |

20 | 21 | 22 | 23 | ``` 24 | git clone https://github.com/saidvandeklundert/pyo3.git 25 | cd pyo3 26 | docker build ./ -t pyo3 27 | docker run --name='pyo3' --hostname='pyo3' -di pyo3:latest 28 | docker exec -it pyo3 bash 29 | cd /opt/pyo3/pyo3 30 | python3 -m venv /opt/venv 31 | . /opt/venv/bin/activate 32 | maturin develop --release 33 | python example.py 34 | ``` 35 | 36 | After updating the code, you can do the following to work with the updates that you made: 37 | 38 | ``` 39 | git pull 40 | python3 -m venv .env 41 | source .env/bin/activate 42 | maturin develop --release 43 | ``` 44 | 45 | ## More information 46 | 47 | For more information, check the PyO3 repository [here](https://github.com/PyO3/pyo3) or check the PyO3 user guide [here](https://pyo3.rs/main/). -------------------------------------------------------------------------------- /pyo3/example.py: -------------------------------------------------------------------------------- 1 | import rust 2 | from timeit import default_timer as timer 3 | from pydantic import BaseModel 4 | 5 | # multiply 6 | result = rust.multiply(2, 3) 7 | print(result) 8 | 9 | # sum of list of numbers: 10 | def sum_list(numbers: list) -> int: 11 | total = 0 12 | for number in numbers: 13 | total += number 14 | return total 15 | 16 | 17 | result = rust.list_sum([10, 10, 10, 10, 10]) 18 | print(result) 19 | 20 | 21 | # Working with different types: 22 | 23 | # word printer: 24 | rust.word_printer("hello", 3, False, True) 25 | rust.word_printer("eyb", 3, True, False) 26 | 27 | # print a list of strings to console 28 | a_list = ["one", "two", "three"] 29 | rust.vector_printer(a_list) 30 | 31 | another_list = ["1", "2", "3", "4", "5", "6", "7", "8"] 32 | rust.array_printer(another_list) 33 | 34 | # print a dictionary to console: 35 | a_dict = { 36 | "key 1": "value 1", 37 | "key 2": "value 2", 38 | "key 3": "value 3", 39 | "key 4": "value 4", 40 | } 41 | 42 | rust.dict_printer(a_dict) 43 | 44 | # the following two functions will fail because 'dict_printer' 45 | # is expecting a dict with string keys and string values: 46 | try: 47 | rust.dict_printer("wrong type") 48 | except TypeError as e: 49 | print(f"Caught a type error: {e}") 50 | 51 | try: 52 | rust.dict_printer({"a": 1, "b": 2}) 53 | except TypeError as e: 54 | print(f"Caught a type error: {e}") 55 | 56 | 57 | # count occurrences of a word in a string: 58 | def count_occurences(contents: str, needle: str) -> int: 59 | total = 0 60 | for line in contents.splitlines(): 61 | for word in line.split(" "): 62 | if word == needle or word == needle + ".": 63 | total += 1 64 | return total 65 | 66 | 67 | text = ( 68 | """🐍 searches through the words. Here are some additional words for 🐍.\nSome words\n""" 69 | * 1000 70 | ) 71 | 72 | 73 | res = count_occurences(text, "words") 74 | print("count_occurences for 'words' in Python:", res) 75 | 76 | rust_res = rust.count_occurences(text, "words") 77 | print("count_occurences for 'words' in Rust:", rust_res) 78 | 79 | start = timer() 80 | res = count_occurences(text, "🐍") 81 | elapsed = round(timer() - start, 10) 82 | print(f"count_occurences for '🐍' in Python took {elapsed}. Result: {res}") 83 | 84 | start = timer() 85 | rust_res = rust.count_occurences(text, "🐍") 86 | elapsed = round(timer() - start, 10) 87 | print(f"count_occurences for '🐍' in Python took {elapsed}. Result: {rust_res}") 88 | 89 | # Calculating fibonacci 90 | from fib import get_fibonacci 91 | 92 | print(f"Fibonacci number in Python and in Rust:") 93 | for i in range(10): 94 | res_python = get_fibonacci(i) 95 | res_rust = rust.get_fibonacci(i) 96 | print(f"number{i}:\t{res_python}\tand in Rust: {res_rust}") 97 | 98 | 99 | py_start = timer() 100 | for i in range(999): 101 | get_fibonacci(150) 102 | 103 | py_res = get_fibonacci(150) 104 | py_elapsed = round(timer() - py_start, 5) 105 | ru_start = timer() 106 | for i in range(999): 107 | rust.get_fibonacci(150) 108 | 109 | ru_res = rust.get_fibonacci(150) 110 | ru_elapsed = round(timer() - ru_start, 5) 111 | print("Calculating the 150th fibonacci number 1000 times.") 112 | print(f"Python took {py_elapsed} seconds and got:\t{py_res}.") 113 | print(f"Rust took {ru_elapsed} seconds and got:\t{ru_res}.") 114 | 115 | # Using a struct that is defined in Rust, a struct called RustStruct: 116 | rust_struct = rust.RustStruct(data="some data", vector=[255, 255, 255]) 117 | 118 | # Calling some methods on the struct: 119 | rust_struct.extend_vector([1, 1, 1, 1]) 120 | rust_struct.printer() 121 | 122 | 123 | # sending over a Pydantic basemodel: 124 | class Human(BaseModel): 125 | name: str 126 | age: int 127 | 128 | 129 | jan = Human(name="Jan", age=6) 130 | rust.human_says_hi(jan.json()) 131 | 132 | # Have Rust use the Python logger: 133 | import logging 134 | 135 | FORMAT = "%(levelname)s %(name)s %(asctime)-15s %(filename)s:%(lineno)d %(message)s" 136 | logging.basicConfig(format=FORMAT) 137 | logging.getLogger().setLevel(logging.DEBUG) 138 | logging.info("Logging from the Python code") 139 | rust.log_example() 140 | rust.log_different_levels() 141 | 142 | # 'handle' a Rust error in Python by catching the exception: 143 | print(rust.greater_than_2(3)) 144 | try: 145 | print(rust.greater_than_2(1)) 146 | except Exception as e: 147 | print(f"Caught an exception: {e}") 148 | print(type(e)) 149 | print("Still going strong.") 150 | -------------------------------------------------------------------------------- /pyo3/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate serde; 2 | extern crate serde_json; 3 | use log::{debug, error, info, warn}; 4 | use pyo3::exceptions::PyOSError; 5 | use pyo3::prelude::*; 6 | use pyo3::wrap_pyfunction; 7 | use pyo3_log; 8 | use serde::{Deserialize, Serialize}; 9 | use std::collections::HashMap; 10 | 11 | use std::fmt; 12 | 13 | /// Multiply two numbers: 14 | #[pyfunction] 15 | fn multiply(a: isize, b: isize) -> PyResult { 16 | Ok(a * b) 17 | } 18 | 19 | /// Return the sum of a list/vector of numbers 20 | #[pyfunction] 21 | fn list_sum(a: Vec) -> PyResult { 22 | let mut sum: isize = 0; 23 | for i in a { 24 | sum += i; 25 | } 26 | Ok(sum) 27 | } 28 | 29 | /// Word printer: 30 | /// Prints a word to the console n number of times. 31 | /// Optionally, the word is printed in reverse and or in uppercase. 32 | #[pyfunction] 33 | fn word_printer(mut word: String, n: isize, reverse: bool, uppercase: bool) { 34 | if reverse { 35 | let mut reversed_word = String::new(); 36 | for c in word.chars().rev() { 37 | reversed_word.push(c); 38 | } 39 | word = reversed_word; 40 | } 41 | if uppercase { 42 | word = word.to_uppercase(); 43 | } 44 | for _ in 0..n { 45 | println!("{}", word); 46 | } 47 | } 48 | 49 | /// Print every item of a list to console: 50 | #[pyfunction] 51 | fn vector_printer(a: Vec) { 52 | for string in a { 53 | println!("{}", string) 54 | } 55 | } 56 | 57 | // Print all the key values in a dict to console: 58 | #[pyfunction] 59 | fn dict_printer(hm: HashMap) { 60 | for (key, value) in hm { 61 | println!("{} {}", key, value) 62 | } 63 | } 64 | 65 | /// Print every item in an array to console: 66 | #[pyfunction] 67 | fn array_printer(a: [String; 8]) { 68 | for string in a { 69 | println!("{}", string) 70 | } 71 | } 72 | 73 | #[pyfunction] 74 | fn count_occurences(contents: &str, needle: &str) -> usize { 75 | let mut count = 0; 76 | for line in contents.lines() { 77 | for word in line.split(" ") { 78 | if word == needle || word == format!("{}.", needle) { 79 | count += 1; 80 | } 81 | } 82 | } 83 | count 84 | } 85 | /// Formats the sum of two numbers as string. 86 | #[pyfunction] 87 | fn sum_as_string(a: usize, b: usize) -> PyResult { 88 | Ok((a + b).to_string()) 89 | } 90 | 91 | #[pyfunction] 92 | fn human_says_hi(human_data: String) { 93 | println!("{}", human_data); 94 | let human: Human = serde_json::from_str(&human_data).unwrap(); 95 | 96 | println!( 97 | "Now we can work with the struct:\n {:#?}.\n {} is {} years old.", 98 | human, human.name, human.age, 99 | ) 100 | } 101 | 102 | #[derive(Debug, Serialize, Deserialize)] 103 | struct Human { 104 | name: String, 105 | age: u8, 106 | } 107 | 108 | #[pyfunction] 109 | fn log_different_levels() { 110 | error!("logging an error"); 111 | warn!("logging a warning"); 112 | info!("logging an info message"); 113 | debug!("logging a debug message"); 114 | } 115 | 116 | #[pyfunction] 117 | fn log_example() { 118 | info!("A log message from {}!", "Rust"); 119 | } 120 | 121 | #[pyfunction] 122 | fn get_fibonacci(number: isize) -> PyResult { 123 | if number == 1 { 124 | return Ok(1); 125 | } else if number == 2 { 126 | return Ok(2); 127 | } 128 | 129 | let mut sum = 0; 130 | let mut last = 0; 131 | let mut curr = 1; 132 | for _ in 1..number { 133 | sum = last + curr; 134 | last = curr; 135 | curr = sum; 136 | } 137 | Ok(sum) 138 | } 139 | 140 | // Raising an exception in a function called 'greater_than_2', which is defined later on. 141 | // Some additional clarifications can be found here: https://blog.burntsushi.net/rust-error-handling/ 142 | 143 | // Define 'MyError' as a custom exception: 144 | #[derive(Debug)] 145 | struct MyError { 146 | /* 147 | the 'message' field that is used later on 148 | to be able print any message. 149 | */ 150 | pub msg: &'static str, 151 | } 152 | 153 | // Implement the 'Error' trait for 'MyError': 154 | impl std::error::Error for MyError {} 155 | 156 | // Implement the 'Display' trait for 'MyError': 157 | impl fmt::Display for MyError { 158 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 159 | write!(f, "Error from Rust: {}", self.msg) 160 | } 161 | } 162 | 163 | // Implement the 'From' trait for 'MyError'. 164 | // Used to do value-to-value conversions while consuming the input value. 165 | impl std::convert::From for PyErr { 166 | fn from(err: MyError) -> PyErr { 167 | PyOSError::new_err(err.to_string()) 168 | } 169 | } 170 | 171 | #[pyfunction] 172 | // The function 'greater_than_2' raises an exception if the input value is 2 or less. 173 | fn greater_than_2(number: isize) -> Result { 174 | if number <= 2 { 175 | return Err(MyError { 176 | msg: "number is less than or equal to 2", 177 | }); 178 | } else { 179 | return Ok(number); 180 | } 181 | } 182 | 183 | #[pyclass] 184 | pub struct RustStruct { 185 | #[pyo3(get, set)] 186 | pub data: String, 187 | #[pyo3(get, set)] 188 | pub vector: Vec, 189 | } 190 | #[pymethods] 191 | impl RustStruct { 192 | #[new] 193 | pub fn new(data: String, vector: Vec) -> RustStruct { 194 | RustStruct { data, vector } 195 | } 196 | pub fn printer(&self) { 197 | println!("{}", self.data); 198 | for i in &self.vector { 199 | println!("{}", i); 200 | } 201 | } 202 | pub fn extend_vector(&mut self, extension: Vec) { 203 | println!("Extending the vector."); 204 | for i in extension { 205 | self.vector.push(i); 206 | } 207 | } 208 | } 209 | 210 | /// A Python module implemented in Rust. The name of this function must match 211 | /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to 212 | /// import the module. 213 | #[pymodule] 214 | fn rust(_py: Python, m: &PyModule) -> PyResult<()> { 215 | pyo3_log::init(); 216 | m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; 217 | m.add_function(wrap_pyfunction!(multiply, m)?)?; 218 | m.add_function(wrap_pyfunction!(list_sum, m)?)?; 219 | m.add_function(wrap_pyfunction!(word_printer, m)?)?; 220 | m.add_function(wrap_pyfunction!(vector_printer, m)?)?; 221 | m.add_function(wrap_pyfunction!(dict_printer, m)?)?; 222 | m.add_function(wrap_pyfunction!(array_printer, m)?)?; 223 | m.add_function(wrap_pyfunction!(count_occurences, m)?)?; 224 | m.add_function(wrap_pyfunction!(human_says_hi, m)?)?; 225 | m.add_wrapped(wrap_pyfunction!(log_example))?; 226 | m.add_wrapped(wrap_pyfunction!(log_different_levels))?; 227 | m.add_function(wrap_pyfunction!(get_fibonacci, m)?)?; 228 | m.add_function(wrap_pyfunction!(greater_than_2, m)?)?; 229 | m.add_class::()?; 230 | 231 | Ok(()) 232 | } 233 | --------------------------------------------------------------------------------