Jython can be embedded into Java applications to, for example, provide scripting capabilities to Java applications or to extend applications with Python modules. "Scripting for the Java Platform", also known as JSR 223, provides a generic mechanism for such integrations. This mechanism has some limitations, but is supported by both Jython and Truffle. However, Jython also provides an API to interact with the Python interpreter directly and migrating such integrations to GraalPy requires more work to match the appropriate APIs.
In this guide, we will show how to migrate from Jython to GraalPy on a Java application that uses Swing for interactive scripting.
To complete this guide, you will need the following:
- Some time on your hands
- A decent text editor or IDE
- A supported JDK1, preferably the latest GraalVM JDK
The demo application we use provides a Swing UI with text input, a label, and a drop-down menu. Each element of the drop-down menu defines how to interpret text inputs, as well as setup and teardown code for the interpreter. The simplest interpreter is "Echo", which needs no setup or teardown and simply returns the input string back:
EchoInputCallback.java
package org.example;
final class EchoInputCallback implements InputCallback {
@Override
public void setUp(Workspace workspace) {
}
@Override
public String interpret(String code) {
return code;
}
@Override
public void tearDown() {
}
}
The Jython entry uses Jython to evaluate the code and return the result.
JythonInputCallback.java
package org.example;
import org.python.core.*;
import org.python.util.PythonInterpreter;
final class JythonInputCallback implements InputCallback {
static PyCode JYTHON_CODE = new PythonInterpreter().compile("__import__('sys').version"); // ①
private PythonInterpreter python;
@Override
public void setUp(Workspace workspace) {
var globals = new PyDictionary();
this.python = new PythonInterpreter(globals); // ②
globals.put(new PyString("this"), workspace);
}
@Override
public String interpret(String code) {
try {
PyObject result;
if (code.isBlank()) {
result = python.eval(JYTHON_CODE);
} else {
result = python.eval(code);
}
if (result instanceof PyString strResult) { // ③
return code + "\n... " + strResult.asString();
}
return code + "\n...(repr) " + result.__repr__();
} catch (PySyntaxError e) {
python.exec(code); // ④
return "";
}
}
@Override
public void tearDown() {
python.close();
}
}
❶ Jython compiles Python code snippets to PyCode objects, which hold bytecode and can be stored and re-used, serialized, and run in different Jython executions without re-parsing.
❷ The PythonInterpreter class is the official API to instantiate Jython. A custom PyDictionary
object can be passed that can be used to share global bindings with Java.
❸ Jython returns evaluation results directly as the implementation types of the Python runtime.
❹ The Jython interpreter exposes the Python-specific difference between eval
of expressions and exec
of statements by raising a syntax error.
We recommend to check out the completed example and follow along with the steps in the next sections that were used to migrate the application step by step.
Add the required dependencies for GraalPy in the <dependencies>
section of the POM.
pom.xml
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>python</artifactId> <!-- ① -->
<version>24.1.2</version>
<type>pom</type> <!-- ② -->
</dependency>
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>polyglot</artifactId> <!-- ③ -->
<version>24.1.2</version>
</dependency>
❶ The python
dependency is a meta-package that transitively depends on all resources and libraries to run GraalPy.
❷ Note that the python
package is not a JAR - it is simply a pom
that declares more dependencies.
❸ The polyglot
dependency provides the APIs to manage and use GraalPy and other Graal languages from Java.
GraalPyInputCallback.java
package org.example;
import org.graalvm.polyglot.*;
final class GraalPyInputCallback implements InputCallback {
static Source GRAALPY_CODE = Source.create("python", "__import__('sys').version");
static Engine engine = Engine.create("python");
private Context python;
@Override
public void setUp(Workspace workspace) {
this.python = Context.newBuilder("python") // ①
.engine(engine) // ②
.allowAllAccess(true).option("python.EmulateJython", "true") // ③
.build();
python.getBindings("python").putMember("this", workspace); // ④
}
❶ The Polyglot API is the generic way to create interpreters of Graal languages, including GraalPy.
❷ Where Jython allows sharing code by sharing PyCode
objects, Graal languages share code by holding on to Source
objects.
JIT compiled code related to a given Source
is shared when running on a Graal JDK if multiple Contexts are configured to share an Engine
.
❸ Jython does not impose access restrictions on the Python code. GraalPy, on the other hand, is locked down by default. Jython also provides some conveniences such as redirecting Python field access to Java object getter methods. For migration it makes sense to remove access restrictions and enable additional Jython heuristics also on GraalPy. After migration, we should try to remove as many permissions and heuristics as possible.
❹ Instead of accessing the Python globals
dictionary, which is a detail of the Python language, bindings offer a language-agnostic way to interact with the global scope.
GraalPyInputCallback.java
@Override
public String interpret(String code) {
Value result;
if (code.isBlank()) {
result = python.eval(GRAALPY_CODE);
} else {
result = python.eval("python", code); // ①
}
if (result.isString()) { // ②
return code + "\n... " + result.asString();
}
return code + "\n...(repr) " + result.toString();
}
❶ Evaluation of code is very similar to how it is done for Jython.
Note, however, that the distinction in the Python language between exec
and eval
is not exposed, so both are represented with the same API call on GraalPy.
❷ GraalPy implementation types are hidden behind the Value
type, so instead of Java typechecks there are APIs to determine what kind of object we are dealing with and to convert it to basic Java types like String
.
GraalPyInputCallback.java
@Override
public void tearDown() {
python.close(true); // ①
}
❶ Closing the Jython interpreter cancels all executions that may be running on other threads, whereas GraalPy tries to wait for executions to finish.
To get the same behavior as Jython, we pass true
to the close
call to cancel executions immediately.
If you downloaded the example, you can now compile and run your application from the commandline:
./mvnw compile
./mvnw exec:java -Dexec.mainClass=org.example.App
You can switch between Jython and GraalPy and compare the output.
Also try to access the this
object that is injected into the top Python scope.
Try to access the this.frame
and you will observe that the getFrame()
method is selected to get the result heuristically.
-
Use GraalPy in a clean-slate Java SE application
-
Use GraalPy with popular Java frameworks, such as Spring Boot or Micronaut
-
Install and use Python packages that rely on native code, e.g. for data science and machine learning
-
Follow along how you can manually install Python packages and files if the Maven plugin gives not enough control
-
Freeze transitive Python dependencies for reproducible builds
-
Learn more about the GraalPy Maven plugin
-
Learn more about the Polyglot API for embedding languages
-
Explore in depth with GraalPy reference manual
Footnotes
-
Oracle JDK 17 and OpenJDK 17 are supported with interpreter only. GraalVM JDK 21, Oracle JDK 21, OpenJDK 21 and newer with JIT compilation. Note: GraalVM for JDK 17 is not supported. ↩