Learn how to run user's python code with Chaquopy in your Android App.

How to use Chaquopy to run Python Code and obtain its output using Java in your Android App

Approximately 4 months have passed since the launch of Pocket Editor for Android. One of the main missing features in the application is the possibility to run some code on the fly. There are a ton of reasons why the implementation of compilers or interpreters have been delayed for so long. One of the main reasons is due to the problematic Storage Access Framework implemented since Android 11 which makes it quite complex to work with files from the user's device, besides of the user's expectations. For example, some users may expect that running Python code directly on the device should automatically give them the rights to manipulate files of the device to its will, thing that is quite difficult to implement, at least for the knowledge I possess right now.

So, although I'll be lowering many users expectations, I decided to proceed with a very useful implementation of a Python interpreter in Pocket Editor thanks to Chaquopy, the Python SDK for Android. Chaquopy provides everything you need to include Python components in an Android app, including:

  • Full integration with Android Studio’s standard Gradle build system.
  • Simple APIs for calling Python code from Java/Kotlin, and vice versa.
  • A wide range of third-party Python packages, including SciPy, OpenCV, TensorFlow and many more.

In this article, I will explain to you how to easily run Python code and obtain its output using Chaquopy in your Android Application.

1. Installing Chaquopy

In most of cases, the installation of Chaquopy is really easy and straightforward, specially with clean (recently created) projects. In my case, I had some trouble with the installation as Chaquopy is quite strict when dealing with the Android Gradle version and I had already an old application that was using a not-so-recent version of Gradle. Remember, if you have trouble when synchronizing your gradle because either the version of Chaquopy can't be found in the maven repository (but the version indeed exists) or the com.chaquo.python namespace can't be accessed, be sure to match the required Android Gradle version with the list of compatible versions in the official Chaquopy documentation here. For example, in the case of my project, I was able to succeed with the installation using the following versions of Gradle:

Gradle Script Version

And requesting the installation of Chaquopy 9.1.0:

classpath 'com.chaquo.python:gradle:9.1.0'

Once you know the version of Gradle of your project and the version of Chaquopy that you can install, you may now proceed with the installation. As specified by the official documentation, Chaquopy is distributed as a plugin for Android’s Gradle-based build system. It can be used in any app which meets the following requirements:

  • In your project’s top-level build.gradle file, the Android Gradle plugin version (com.android.tools.build:gradle) should be between 3.6 and 7.0. Older versions as far back as 2.2 are supported by older versions of Chaquopy.

  • minSdkVersion must be at least 16. Older versions as far back as 15 are supported by older versions of Chaquopy.

Meeting this criteria, you need now to register the maven repository of Chaquopy and include it as a dependency in your top level build.gradle file:

buildscript {
    repositories {
        maven { url "https://chaquo.com/maven" }
    }
    dependencies {
        classpath "com.chaquo.python:gradle:9.1.0"
    }
}

As I mentioned previously, I used the version 9.1.0 of Chaquopy to make it work in my project, yours could work with the latest version of Chaquopy (10.0.1 till the date of writing this article). After making this change to the Gradle file, apply the com.chaquo.python plugin under the com.android.application plugin. If you are using the apply syntax:

apply plugin: 'com.android.application'
apply plugin: 'com.chaquo.python'

Or if you are using the plugin syntax:

plugins {
    id 'com.android.application'
    id 'com.chaquo.python'
}

That's all you need for the installation for now, do not synchronize your project with Gradle Files yet as you need to configure some stuff before.

2. Configure Chaquopy

Now, in your app's build.gradle you need to configure the basic settings of Chaquopy. These settings are the following:

apply plugin: 'com.android.application'
apply plugin: 'com.chaquo.python'

android {
    ...

    defaultConfig {
        
        applicationId "com.yourcompany.yourappid"

        ...

        ndk {
            abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
        }

        sourceSets{
            main{
                python.srcDir "src/main/python"
            }
        }

        python {
            buildPython "C:/Python38/python.exe"
        }
    }
}
  • ndk (abiFilters): The Python interpreter is a native component, so you must use the abiFilters setting to specify which ABIs you want the app to support. The currently available ABIs are:
    • armeabi-v7a, supported by virtually all Android devices.

    • arm64-v8a, supported by most recent Android devices.

    • x86, for the Android emulator.

    • x86_64, for the Android emulator.

  • sourceSets: By default, Chaquopy uses the default python subdirectory of each source set. For example the Python Code of the main source should be stored in the app/src/main/python directory.
  • buildPython: Some features require Python to be installed on the build machine. By default, Chaquopy will try to find Python on the PATH with the standard command for your operating system, first with a matching minor version, and then with a matching major version. In my case using Windows, I provided the absolute path to the executable with the same Python version of Chaquopy 9.1.0 (Python 3.8.6).

Finally, synchronize your project with Gradle Files and wait for the build to finish. Once it finishes, you will be able to use Chaquopy in your project.

3. Building interpreter

As mentioned at the beginning of the article, the idea of the app is to run python code provided by the user and obtain the output. For example, if the user provides the following Python code to print the Fibonacci series:

def fibonacci_of(n):
    if n in {0, 1}:  # Base case
        return n

    return fibonacci_of(n - 1) + fibonacci_of(n - 2)
    
print([fibonacci_of(n) for n in range(10)])

The application should be able to capture the output that would be in this case:

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

After reading the documentation, I was unable to found a way to run code directly without so much hassle using Chaquopy (there's no simple method like executeCode or something like that). Instead, I followed a curious approach that would run the code through a Python script that will receive the code and will run it on another thread. The following Python script will allow us to execute arbitrary code using the python exec function. I obtained this script from this implementation of Chaquopy in Flutter (see official repository here):

# Source: https://github.com/jaydangar/chaquopy_flutter/blob/5fefecba7c918ce4c0e790a1a6494e70a701059c/example/android/app/src/main/python/script.py
# interpreter.py
import time,threading,ctypes,inspect

def _async_raise(tid, exctype):
    tid = ctypes.c_long(tid)
    if not inspect.isclass(exctype):
        exctype = type(exctype)
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
    if res == 0:
        raise ValueError("Invalid thread id")
    elif res != 1:
        ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
        raise SystemError("Timeout Exception")

def stop_thread(thread):
    _async_raise(thread.ident, SystemExit)
    
def text_thread_run(code):
    try:
        env={}
        exec(code, env, env)
    except Exception as e:
        print(e)
    
#   This is the code to run Text functions...
def mainTextCode(code):
    global thread1
    thread1 = threading.Thread(target=text_thread_run, args=(code,),daemon=True)
    thread1.start()
    # Define the
    timeout = 15 # change timeout settings in seconds here...
    thread1_start_time = time.time()
    while thread1.is_alive():
        if time.time() - thread1_start_time > timeout:
            stop_thread(thread1)
            raise TimeoutError
        time.sleep(1)

Be sure to create the interpreter.py file inside your Android directory src/main/python and save the previous code on it. Considering the way in which Chaquopy operates, we need to create a PyObject that will use the interpreter.py file as a module and we will run the code using the mainTextCode method of the python script. The following logic will do the trick in your Android Application to run Python code from a plain string:

import com.chaquo.python.PyException;
import com.chaquo.python.PyObject;
import com.chaquo.python.Python;
import com.chaquo.python.android.AndroidPlatform;

// 1. Start the Python instance if it isn't already running.
if(!Python.isStarted()){
    Python.start(new AndroidPlatform(getContext()));
}

// 2. Obtain the python instance
Python py = Python.getInstance();

// 3. Declare some Python code that will be interpreted
// In our case, the fibonacci sequence
String code = "def fibonacci_of(n):\n";
code += "   if n in {0, 1}:  # Base case\n";
code += "       return n\n";
code += "   return n\n";
code += "\n";
code += "print([fibonacci_of(n) for n in range(10)])\n";

// 4. Obtain the system's input stream (available from Chaquopy)
PyObject sys = py.getModule("sys");
PyObject io = py.getModule("io");
// Obtain the interpreter.py module
PyObject console = py.getModule("interpreter");

// 5. Redirect the system's output stream to the Python interpreter
PyObject textOutputStream = io.callAttr("StringIO");
sys.put("stdout", textOutputStream);


// 6. Create a string variable that will contain the standard output of the Python interpreter
String interpreterOutput = "";

// 7. Execute the Python code
try {
    console.callAttrThrows("mainTextCode", code);

    interpreterOutput = textOutputStream.callAttr("getvalue").toString();
}catch (PyException e){
    // If there's an error, you can obtain its output as well
    // e.g. if you mispell the code
    // Missing parentheses in call to 'print'
    // Did you mean print("text")?
    // <string>, line 1
    interpreterOutput = e.getMessage().toString();
} catch (Throwable throwable) {
    throwable.printStackTrace();
}

// Outputs in the case of the fibonacci script:
// "[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]"
System.out.println(interpreterOutput);

And that's how you can run Python code dynamically in your Android application using Chaquopy.

Happy coding ❤️!


Senior Software Engineer at Software Medico. Interested in programming since he was 14 years old, Carlos is a self-taught programmer and founder and author of most of the articles at Our Code World.

Sponsors