The Way to Speed Up your Python Apps is Rust..

Crafted by Mustapha BEN on June 8, 2023

We all encountered a situation where Python was pushed to its limits.. This happens usually when we are dealing with heavy computation..

There are many ways to address this issue, for instance :

  1. Optimizing algorithmically the implementation ;
  2. Using some mature libraries such as NumPy, SciPy or pandas ;
  3. Calling for Cython help !
  4. Using Rust and wrap it with your Python code ;
  5. Etc.

I know.. I know.. I know.. Why bothering with 4. when we can just use 2. and 3.

Give Me A Reason..

We agree that coding in Python is a real joy.. You are equipped with powerful tools which make you highly productive in many use cases.. Despite this fact, there are some restricted environments when speed is crucial.. At MINDIMENSIONS, we started a project of creating a Demand Side Platform (DSP) which uses Artificial Intelligence during the Real-Time Biding (RTB) process. The POC was straightforward thanks to Python ecosystem. But putting the system into production was another story.

As it turns out, before adding any fancy feature to our platform, this one should execute the whole process in no more than the same duration that a website can take in order to be fully loaded. At this stage, we changed our vocabulary and we begun talking about Java, Golang, Scala and Rust.

But, what if we just want to delegate the bottleneck parts to another language and stick with Python for the fancy stuff ?! Yup.. This is when Rust comes in..

Show Me Some Code..

So, how we can put this in practice.. Let's simplify our problem and describe it as follows :

  • We have a Python application which is, in our example, a Django API ;
  • Our API makes some heavy computation which can be modeled by computing recursively the Fibonacci number. The choice of this algorithm is because Fibonacci sequence is easy to implement and the recursive version has an exponential complexity ;
  • Rust code will be wrapped as a package and then be used by our API.

The Python Realm

Let's start by creating a virtual environment just as a good Pythonista citizen should be.. Here I am using conda :

$ conda create -n rust_venv_3-10 python=3.10

$ conda activate rust_venv_3-10

Then installing some requirements :

$ pip install Django==4.2.1 djangorestframework==3.14.0

And starting our Django project:

$ django-admin startproject django_rust_api

We create our API application inside apps folder:

$ cd django_rust_api

$ mkdir -p apps/compute

$ python manage.py startapp compute apps/compute/

Inside the Django settings, we add DRF and our app named compute to installed Apps :

INSTALLED_APPS = [
    ...
    # third party apps
    "rest_framework",
    # local apps
    "apps.compute",
    ...
]

Then we tweak the usual files :

apps/compute/views.py

import time

from rest_framework.response import Response
from rest_framework.views import APIView


class ComputeFibonacci(APIView):

    def fibonacci(self, number: int) -> int:
        if number <= 0:
            raise ValueError("The number should be a non-null positive integer")
        if number in range(1, 3):
            return 1
        return self.fibonacci(number - 1) + self.fibonacci(number - 2)

    def get(self, request, *args, **kwargs):

        number = int(self.request.query_params.get("number"))

        start_time = time.time()
        result = self.fibonacci(number)
        end_time = time.time()

        return Response(dict(
                    language="Python",
                    result=result,
                    computing_time=f"{end_time - start_time:0.4f} s",
                ))


compute_fibonacci = ComputeFibonacci.as_view()

apps/compute/urls.py

from django.urls import path
from .views import compute_fibonacci


app_name = "compute"

urlpatterns = [
    path("fibonacci", view=compute_fibonacci, name="compute_fibonacci"),
]

django_rust_api/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("api/compute/", include("apps.compute.urls", namespace="compute")),
]

Now we can run our server $ python manage.py runserver and consume our endpoint http://localhost:8000/api/compute/fibonacci?number=20 which takes number as a query parameter then returns the Fibonacci value and the computing time.

Computing fibonacci(20) is very fast (less than a quarter of a second) but fibonacci(40) takes more than 25 seconds (and depending on the used machine). Then, we rapidly understand that this is a path one should not follow..

Responsive image

So, let's see how we can reduce this time by just changing a few lines of code by making some calls to a Rust implementation. But first, let's create our Rust package then see how we can wrap it inside our Python code.

The Rust Realm

Here we need Rust to be installed and we will use Cargo as a Package Manager. Then inside our apps folder $ cargo new fibonacci_rust this will create

  1. A Cargo.toml file which is the manifest of the package
  2. src folder which will be the source of our Rust code. By default, Cargo adds a main.rs file which is the entry-point of our Rust package.

Inside fibonacci_rust folder, we can run our Rust program by $ cargo run ; this should display a hello world message. Now let's implement the recursive version of Fibonacci using Rust.

As a mapping between Python and Rust, let's create a package named computing_fibonacci Inside fibonacci_rust folder and adding a mod.rs file which has the same behavior as __init__.py . Then, we add another Rust module named recursive.rs in which we will implement our Fibonacci function.

Those files should look like :

apps/fibonacci_rust/src/computing_fibonacci/recursive.rs

pub fn fibonacci(number: i32) -> u64 {
	if number <= 0 {
		panic!("Please provide a positive integer starting with 1");
	}
	match number {
		1 | 2 => 1,
		_     => fibonacci(number - 1) + fibonacci(number - 2)
	}
}

apps/fibonacci_rust/src/computing_fibonacci/mod.rs

pub mod recursive;

Now we have a Rust implementation of Fibonacci, but how we can call this function from a Python code. In order to do so, we should package our Rust code and install it with pip

The Neutral Realm

Let's create a setup.py file at the level of apps/fibonacci_rust/

#!/usr/bin/env python
from setuptools import dist
dist.Distribution().fetch_build_eggs(['setuptools_rust'])
from setuptools import setup
from setuptools_rust import Binding, RustExtension


setup(
    name="fibonacci_rust",
    version="0.1",
    rust_extensions=[RustExtension(
        ".fibonacci_rust.fibonacci_rust",
        path="Cargo.toml", binding=Binding.PyO3)],
    packages=["fibonacci_rust"],
    zip_safe=False,
)

Then we should install the Setuptools plugin with $ pip install setuptools-rust and modify our Cargo.toml file :

[package]
name = "fibonacci_rust"
version = "0.1.0"
edition = "2021"

[lib]
name = "fibonacci_rust"
# "cdylib" is necessary to produce a shared library for Python to import from.
#
# Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able
# to `use string_sum;` unless the "rlib" or "lib" crate type is also included, e.g.:
# crate-type = ["cdylib", "rlib"]
crate-type = ["cdylib", "rlib"]

[dependencies]

[dependencies.pyo3]
version = "0.15.1"
features = ["extension-module"]

Then changing the name of our entrypoint to lib.rs . We should then create a binding between Rust and Python using PyO3. This process should modify two files :

apps/fibonacci_rust/src/computing_fibonacci/recursive.rs

use pyo3::prelude::pyfunction;


#[pyfunction]
pub fn fibonacci(number: i32) -> u64 {
	if number <= 0 {
		panic!("Please provide a positive integer starting with 1");
	}
	match number {
		1 | 2 => 1,
		_     => fibonacci(number - 1) + fibonacci(number - 2)
	}
}

apps/fibonacci_rust/src/lib.rs

use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

mod computing_fibonacci;

use computing_fibonacci::recursive::__pyo3_get_function_fibonacci;


#[pymodule]
fn fibonacci_rust(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pyfunction!(fibonacci));
    Ok(())
}

And in order to create our Python entry-point, we just need to create a new Python package containing just __init__.py and import the Rust module :

apps/fibonacci_rust/fibonacci_rust/__init__.py

from .fibonacci_rust import *

Finally, inside our apps folder, let's install our Rust package using $ pip install fibonacci_rust/, then we can called it from our Python code.

To do so, we will modify apps/compute/views.py and enjoy the benchmarking ;)

import time

from rest_framework.response import Response
from rest_framework.views import APIView

from fibonacci_rust import fibonacci_rust


class ComputeFibonacci(APIView):

    supported_languages = ["Python", "Rust"]

    def fibonacci(self, number: int) -> int:
        if number <= 0:
            raise ValueError("The number should be a non-null positive integer")
        if number in range(1, 3):
            return 1
        return self.fibonacci(number - 1) + self.fibonacci(number - 2)

    def get(self, request, *args, **kwargs):

        number = int(self.request.query_params.get("number"))
        context = []
        for lang in self.supported_languages:
            match lang:
                case "Python":
                    start_time = time.time()
                    result = self.fibonacci(number)
                    end_time = time.time()
                case "Rust":
                    start_time = time.time()
                    result = fibonacci_rust.fibonacci(number)
                    end_time = time.time()

            context.append(
                dict(
                    language=lang,
                    result=result,
                    computing_time=f"{end_time - start_time:0.4f} s",
                )
            )

        return Response(context)


compute_fibonacci = ComputeFibonacci.as_view()
Responsive image

The entire code can be found at https://github.com/Benakrab/django_rust_api

Share This Post