Java
Python
Interoperability
Programming
Integration

Calling Python in Java?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

There are many scenarios where you need to call Python code from a Java application: leveraging a Python machine learning library, reusing an existing Python script, or integrating with a Python-based microservice. Java does not natively execute Python, so you need a bridge between the two runtimes. This article covers four practical approaches, each suited to different requirements around performance, compatibility, and complexity.

Using ProcessBuilder and Runtime.exec

The simplest way to run Python from Java is to spawn a separate Python process using ProcessBuilder or Runtime.exec(). This treats Python as an external command, just like running it from a terminal.

java
1import java.io.BufferedReader;
2import java.io.InputStreamReader;
3
4public class PythonCaller {
5    public static void main(String[] args) throws Exception {
6        ProcessBuilder pb = new ProcessBuilder("python3", "script.py", "arg1", "arg2");
7        pb.redirectErrorStream(true);
8        Process process = pb.start();
9
10        BufferedReader reader = new BufferedReader(
11            new InputStreamReader(process.getInputStream())
12        );
13        String line;
14        while ((line = reader.readLine()) != null) {
15            System.out.println(line);
16        }
17
18        int exitCode = process.waitFor();
19        System.out.println("Python exited with code: " + exitCode);
20    }
21}

The Python script receives arguments through sys.argv and communicates results back through standard output. You can also write to the process's stdin for bidirectional communication.

This approach has zero dependencies and works with any Python version installed on the system. The downside is overhead: each invocation starts a new OS process, and data exchange is limited to text over stdin/stdout. It is best suited for infrequent calls or batch-style workloads where startup cost does not matter.

Using Jython

Jython is an implementation of Python 2.7 that runs directly on the JVM. It compiles Python source code into Java bytecode, which means Python objects and Java objects can interact in the same process without serialization.

java
1import org.python.util.PythonInterpreter;
2import org.python.core.PyObject;
3
4public class JythonExample {
5    public static void main(String[] args) {
6        PythonInterpreter interpreter = new PythonInterpreter();
7
8        interpreter.exec("result = sum(range(1, 101))");
9        PyObject result = interpreter.get("result");
10
11        System.out.println("Sum: " + result.asInt());  // prints 5050
12    }
13}

Add Jython to your project via Maven:

xml
1<dependency>
2    <groupId>org.python</groupId>
3    <artifactId>jython-standalone</artifactId>
4    <version>2.7.3</version>
5</dependency>

The major limitation of Jython is that it only supports Python 2.7. It cannot run code that uses Python 3 syntax or CPython C extensions like NumPy, pandas, or TensorFlow. If your Python code is pure Python 2 and does not depend on C-based libraries, Jython provides the tightest integration with zero inter-process overhead.

Using GraalPython on GraalVM

GraalVM is a polyglot virtual machine that can run Java, JavaScript, Python, Ruby, and other languages in the same runtime. GraalPython is its Python 3 implementation, and it supports a growing subset of the CPython ecosystem.

java
1import org.graalvm.polyglot.Context;
2import org.graalvm.polyglot.Value;
3
4public class GraalPythonExample {
5    public static void main(String[] args) {
6        try (Context context = Context.newBuilder("python")
7                .allowAllAccess(true)
8                .build()) {
9
10            context.eval("python",
11                "def fibonacci(n):\n" +
12                "    a, b = 0, 1\n" +
13                "    for _ in range(n):\n" +
14                "        a, b = b, a + b\n" +
15                "    return a\n"
16            );
17
18            Value fibonacci = context.getBindings("python").getMember("fibonacci");
19            long result = fibonacci.execute(20).asLong();
20            System.out.println("Fibonacci(20) = " + result);  // prints 6765
21        }
22    }
23}

GraalPython runs inside the same JVM process and can achieve good performance through GraalVM's JIT compiler. It supports Python 3 syntax and is steadily adding compatibility with popular packages. However, it does not yet support every CPython C extension, so you should verify that your specific dependencies work before committing to this approach.

Using Py4J and JPype

Py4J and JPype take opposite approaches to the same problem. Py4J lets Python call Java (and Java call back into Python) over a socket connection between two separate processes. JPype embeds CPython directly into the JVM process using JNI.

Py4J example (Java side starts a gateway server):

java
1import py4j.GatewayServer;
2
3public class MathService {
4    public double squareRoot(double value) {
5        return Math.sqrt(value);
6    }
7
8    public static void main(String[] args) {
9        GatewayServer server = new GatewayServer(new MathService());
10        server.start();
11        System.out.println("Gateway server started");
12    }
13}

Python side connects to the gateway:

python
1from py4j.java_gateway import JavaGateway
2
3gateway = JavaGateway()
4result = gateway.entry_point.squareRoot(16.0)
5print(result)  # prints 4.0

JPype example (Python embeds JVM):

python
1import jpype
2import jpype.imports
3
4jpype.startJVM(classpath=['myapp.jar'])
5
6from java.util import ArrayList
7
8lst = ArrayList()
9lst.add("hello")
10lst.add("world")
11print(lst.size())  # prints 2
12
13jpype.shutdownJVM()

Py4J is well-known in the data engineering world because Apache Spark uses it internally for PySpark. It works with any CPython installation and all C extensions, but the socket-based communication adds latency. JPype avoids that overhead by embedding CPython in-process, giving you direct access to the full CPython ecosystem including NumPy and pandas. The trade-off is more complex setup and potential issues with native library conflicts.

Common Pitfalls

  • Hardcoding the Python executable path. Using "python" instead of "python3" in ProcessBuilder can invoke Python 2 on systems where both are installed. Always specify the full path or use "python3" explicitly.
  • Ignoring the error stream in ProcessBuilder. If you only read stdout, Python exceptions and tracebacks are silently discarded. Use redirectErrorStream(true) or read stderr separately.
  • Choosing Jython for Python 3 code. Jython only supports Python 2.7. Attempting to run Python 3 syntax like f-strings or print() as a function with keyword arguments will fail.
  • Not managing the GraalVM context lifecycle. Forgetting to close the Context object leads to resource leaks. Always use try-with-resources as shown in the example.
  • Assuming Py4J calls are low-latency. Each method call crosses a socket boundary. If you need to call Python thousands of times in a tight loop, batch the work into a single Python function call instead.

Summary

  • ProcessBuilder/Runtime.exec is the simplest approach with no dependencies, best for infrequent or batch calls to any Python version.
  • Jython provides tight in-process integration but is limited to Python 2.7 and pure-Python libraries.
  • GraalPython supports Python 3 on the JVM with growing ecosystem compatibility and strong JIT performance.
  • Py4J bridges Java and CPython over sockets, supports all C extensions, and is battle-tested in Apache Spark.
  • JPype embeds CPython directly in the JVM for low-latency access to the full Python ecosystem, at the cost of more complex setup.

Course illustration
Course illustration

All Rights Reserved.