Python source builds as a Codename One application. The build translates each module to Java, compiles it with the rest of the project, and from there it goes wherever Java goes: iOS, Android, the JavaScript port, the desktop ports, the simulator, and native server binaries built on the Codename One backend.
No Python interpreter ships with the application. Each module becomes a Java class, each function a method, and a small runtime supplies Python’s object model, its built-in types and the standard library modules applications use. Annotated code is compiled to unboxed Java arithmetic, so a typed numeric loop runs at the speed of the equivalent Java.
Two things follow from compiling ahead of time rather than interpreting:
The program is known in full at build time. A module imported by a name computed while the program runs,
exec,evalandcompileof a string, and frame introspection are build errors that name the file and the line. None of them reaches a device as a hidden failure.Packages with C extensions are supported. The package’s Python is translated like your own code, and each compiled module is replaced by a Java class with the same functions; see C extensions. The most used ones —
numpy,pydantic's core,yaml's scanner — are already provided, as Codename One implementations of the same public API.
Adding Python to a project
Put the sources in common/src/main/python, or import an existing Python
project into a Codename One application:
mvn cn1:import-python-project -Dsource=/path/to/my-python-app
The import copies the .py files, adds the Python runtime dependency and the
transpile-python goal to the common module, records the entry module and
makes the application’s main class run it. Then build and run the project as
usual; the translation runs in generate-sources. Gradle projects get the
same step as the transpilePython task.
The goal reads these properties:
| Property | Meaning |
|---|---|
| The Python sources, |
| The module run as |
| Where the generated Java goes. |
|
|
Debugging
The translated classes carry the Python source’s file and line numbers in place of the generated Java’s. A stack trace names the Python file and line:
com.codename1.python.runtime.PyException: ValueError: bad 6 at com.codename1.generated.python.py_app$2eutil.M_L1_Thing_boom(util.py:7) at com.codename1.generated.python.py_main.lambda$exec$1(main.py:6) at com.codename1.generated.python.py_main.exec(main.py:10)
The same lines reach a Java debugger attached to the simulator or to a server
binary run on the JVM. Each class also carries a JSR-45 source map whose
default stratum, Python, gives the file’s path within the source directory,
for example app/util.py. Breakpoints are set by Python line number, and a
debugger that resolves sources through the source map opens the .py file.
With jdb:
jdb -sourcepath common/src/main/python -classpath ... MyAppStub
stop at com.codename1.generated.python.py_app$2eutil:6
The mapping is applied to the compiled classes by the build’s remap-compat
step (cn1RemapCompat in Gradle), so the generated Java in
target/generated-sources/python still shows the Python line that
each statement came from, as a /* line N */ comment above it.
Calling Codename One and Java from Python
Any class on the project’s class path is importable under the java. prefix:
from java.com.codename1.ui import Form, Button, Label, TextField, Dialog
from java.com.codename1.ui.layouts import BoxLayout
def main():
form = Form("Hello Python", BoxLayout.y())
name = TextField("", "Your name")
out = Label("Hello, world!")
def on_greet(event):
out.setText(f"Hello, {name.getText().strip()}!")
form.revalidate()
button = Button("Greet")
button.addActionListener(on_greet)
form.add(name).add(button).add(out)
form.show()
main()
A Python function or lambda passed where Java expects a listener or another single-method interface becomes an implementation of that interface. Overloads are chosen at build time when the argument types are known, and otherwise by the runtime from the values passed.
Values cross the boundary as follows:
| Python | Java |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Java collections behave as their Python counterparts in Python code: len,
indexing, in and iteration work on an ArrayList or a HashMap, and a
Java exception arrives as a Python exception whose message names the Java
type.
To call into Python from Java, import the module through the runtime and call
its attributes. The build generates
com.codename1.generated.python.PythonModules, which registers every
translated module. When Python is the application’s entry point, its main
class calls PythonModules.registerAll() at startup; a Java application
that uses Python as a library calls it once before importing anything:
PyModule pricing = Py.importModule("pricing");
Object total = Py.call(Py.getattr(pricing, "total"), cart);
Building the user interface in Python
The user interface is Codename One’s, called from Python, so everything in
the rest of this guide applies: layouts, themes and CSS, the component
inspector, and the same look on every platform. The examples below are
complete applications. Each is a main.py you can put in
common/src/main/python.
A list loaded from the network
The first example fetches JSON with requests on a worker thread, shows a
progress indicator while it waits and fills a list when the response
arrives. Pull the list down, or tap the toolbar button, to load it again.
import threading
import requests
from java.com.codename1.components import InfiniteProgress, MultiButton
from java.com.codename1.ui import Display, FontImage, Form
from java.com.codename1.ui.layouts import BoxLayout
API = "https://api.github.com/repos/codenameone/CodenameOne/releases"
def fetch_releases():
response = requests.get(API, params={"per_page": 20}, timeout=15)
response.raise_for_status()
return [(r["name"] or r["tag_name"], r["published_at"][:10]) for r in response.json()]
def show_releases(form, releases):
form.removeAll()
for name, date in releases:
row = MultiButton(name)
row.setTextLine2(date)
row.addActionListener(lambda e, n=name: Display.getInstance().execute(
"https://github.com/codenameone/CodenameOne/releases/tag/" + n))
form.add(row)
form.revalidate()
def load(form):
progress = InfiniteProgress().showInfiniteBlocking()
def work():
try:
releases = fetch_releases()
except requests.RequestException as e:
releases = [("Couldn't load releases", str(e))]
# Components belong to the EDT: hand the result back to it.
Display.getInstance().callSerially(lambda: (progress.dispose(), show_releases(form, releases)))
threading.Thread(target=work, daemon=True).start()
def main():
form = Form("Releases", BoxLayout.y())
form.getToolbar().addMaterialCommandToRightBar("", FontImage.MATERIAL_REFRESH, lambda e: load(form))
form.getContentPane().addPullToRefresh(lambda: load(form))
form.show()
load(form)
main()
Three conventions carry over from Codename One’s Java API:
Network calls run off the event dispatch thread. A Python thread is fine for that, and
callSeriallyhands the result back to the thread that owns the components.A component added to a form that’s already showing appears after
revalidate().A Python function goes wherever an
ActionListeneror aRunnableis expected. A default argument, as inlambda e, n=name: …, captures the loop’s value for each row, as it would in any Python callback.
A model, two forms and storage
The second example keeps notes in SQLite through the standard sqlite3
module, models them as a dataclass and moves between a list and an editor
form. The editor saves when the user goes back, and asks before deleting.
import sqlite3
from dataclasses import dataclass
from datetime import date
from java.com.codename1.components import MultiButton
from java.com.codename1.ui import Button, Dialog, FontImage, Form, TextArea, TextField
from java.com.codename1.ui.layouts import BorderLayout, BoxLayout
@dataclass
class Note:
id: int | None
title: str
body: str
day: str
class Notes:
def __init__(self, path: str = "notes.db"):
self.db = sqlite3.connect(path)
self.db.execute("CREATE TABLE IF NOT EXISTS note"
" (id INTEGER PRIMARY KEY, title TEXT, body TEXT, day TEXT)")
def all(self) -> list[Note]:
rows = self.db.execute("SELECT id, title, body, day FROM note ORDER BY id DESC")
return [Note(*row) for row in rows]
def save(self, note: Note) -> None:
with self.db:
if note.id is None:
self.db.execute("INSERT INTO note (title, body, day) VALUES (?, ?, ?)",
(note.title, note.body, note.day))
else:
self.db.execute("UPDATE note SET title = ?, body = ? WHERE id = ?",
(note.title, note.body, note.id))
def delete(self, note: Note) -> None:
with self.db:
self.db.execute("DELETE FROM note WHERE id = ?", (note.id,))
class NotesApp:
def __init__(self):
self.store = Notes()
self.home = Form("Notes", BoxLayout.y())
self.home.getToolbar().addMaterialCommandToRightBar("", FontImage.MATERIAL_ADD,
lambda e: self.edit(Note(None, "", "", str(date.today()))))
self.refresh()
def refresh(self):
self.home.removeAll()
for note in self.store.all():
row = MultiButton(note.title or "Untitled")
row.setTextLine2(note.day)
row.addActionListener(lambda e, n=note: self.edit(n))
self.home.add(row)
self.home.revalidate()
def edit(self, note: Note):
form = Form("Edit", BorderLayout())
title = TextField(note.title, "Title")
body = TextArea(note.body, 8, 40)
form.add(BorderLayout.NORTH, title).add(BorderLayout.CENTER, body)
def back(e):
note.title, note.body = title.getText(), body.getText()
self.store.save(note)
self.refresh()
self.home.showBack()
form.getToolbar().setBackCommand("", back)
if note.id is not None:
def delete(e):
if Dialog.show("Delete", f"Delete '{note.title}'?", "Delete", "Cancel"):
self.store.delete(note)
self.refresh()
self.home.showBack()
delete_button = Button("Delete")
delete_button.addActionListener(delete)
form.add(BorderLayout.SOUTH, delete_button)
form.show()
def start(self):
self.home.show()
NotesApp().start()
The structure is the one a Python developer would write for any user
interface toolkit: a storage class with no user interface code, and a class
that owns the forms and turns model objects into components. show()
moves forward to a form, showBack() returns with the reverse transition,
and the toolbar’s back command is also what Android’s back button runs.
Libraries
The standard library modules applications use most are included: json, re,
datetime, decimal, fractions, collections, itertools, functools,
dataclasses, enum, typing, inspect, asyncio, logging, pathlib,
os.path, csv, struct, hashlib, base64, uuid, random,
statistics, heapq, bisect, textwrap, string, argparse, tomllib, sqlite3,
threading, queue, concurrent.futures, pickle, shelve,
urllib.request, http.client, email.message, zlib, gzip, zipfile,
zoneinfo, unittest (with unittest.mock) and others. A small pytest is
included as well: raises, approx, marks, fixtures and parametrized tests.
These third party packages are provided, each tested against the output of the real package:
| Package | Notes |
|---|---|
| Over Codename One’s network stack. |
| Models, fields, validators, |
| |
|
|
|
|
|
|
| See Python backends. |
|
Packages from PyPI
cn1:import-python-project installs the project’s requirements, read from
pyproject.toml and requirements.txt, from PyPI. Each requirement and
everything it depends on is resolved to the newest release its version
specifiers accept, its download is checked against the SHA-256 PyPI
publishes, and its Python sources go into common/src/main/python-packages.
The build translates that directory together with src/main/python.
Requirements on packages the runtime already provides, such as requests
or numpy, aren’t downloaded.
The versions chosen are recorded in python-packages/python.lock. Importing
again installs the same versions, so a dependency changes only when you
delete its line from the lock, or the file itself, and import again.
| Property | Meaning |
|---|---|
|
|
| Another index that serves PyPI’s JSON API, such as a company mirror. |
You can also copy a package’s sources into the source directory by hand; they translate like the application’s own modules.
C extensions
A package with a C extension is supported. Its compiled modules are what CPython loads from C; everything around them is ordinary Python. The build translates that Python as usual, and each compiled module is replaced by a Java class that registers the same functions. Most C extensions are small accelerators or thin wrappers over a system facility, so the Java is usually short, and often a call into a Codename One API that already does the job.
The import finds the compiled modules (.so and .pyd files in a wheel,
.pyx sources in a source distribution) and the names the package’s Python
uses from each one. For every module it generates a class in
common/src/main/java/com/codename1/python/ext with a stub per function,
and lists it in python-packages/cn1-native-modules.properties:
public final class MarkupsafeSpeedups {
public static void install() {
new NativeModule("markupsafe._speedups")
.function("escape", args -> {
throw NativeModule.notImplemented("markupsafe._speedups.escape");
})
.install();
}
}
Replace each stub’s body with Java. The functions receive plain Java values
(a Number for an int or a float, String, byte[], java.util.List
for a list or tuple, java.util.Map for a dict, null for None) and
return the same kinds; an Object[] comes back to Python as a tuple. Raise a
Python exception with Py.error(Py.TYPE_ERROR, "…") and its siblings, and
use NativeModule.raw for a function that needs the Python objects
themselves. Until a stub is filled in, calling it raises a
NotImplementedError that names it. The import never overwrites a class
that already exists, so importing again keeps your code.
Many packages ship a C accelerator together with a pure Python version of
the same code, and import the compiled module inside
try: … except ImportError:. When every import of a compiled module is
guarded that way, the import reports it as optional and generates nothing:
the pure Python version runs instead.
Threads
threading, queue and concurrent.futures run on Java threads. As in
CPython, one global interpreter lock serializes Python code: it’s released
while a thread sleeps, waits on a lock, a queue or a future, or waits for a
network response, so threads help with blocking work and not with
computation. ProcessPoolExecutor is accepted and runs its tasks on threads.
Codename One’s user interface belongs to its event dispatch thread. Python
code started by the application runs on that thread, so update components
from a worker thread with Display.getInstance().callSerially, as Java code
does.
sqlite3
sqlite3 runs on the platform’s own SQLite through Codename One’s
Database. On iOS, Android and the simulator a value comes back in the
storage class SQLite recorded for it. The Linux, Windows and JavaScript ports
don’t report storage classes, so a value comes back as an int or a float
when its text is a number, and as a str otherwise.
Python backends
A Flask or FastAPI application builds into a single native server executable. The application starts the server the way it always does:
from flask import Flask, jsonify
app = Flask(__name__)
@app.get("/fib/<int:n>")
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return jsonify({"n": n, "fib": str(a)})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
app.run() and uvicorn.run(app) are lowered onto the Codename One backend’s
HTTP server, so the same file still runs under CPython for development:
scripts/python-backend/build.sh scripts/python-backend/samples/flask-todo ./todo-server
./todo-server
Routing, converters, JSON bodies, abort, error handlers and the default
error pages of Flask, and FastAPI’s path, query, body and header parameters,
dependencies, response_model and OpenAPI generation are supported, and
Flask sessions, flash, Jinja templates (extends, block, include,
macros, call, import, filter blocks, loops, conditions and the common
filters, with autoescaping), multipart file uploads and streamed responses
work as well. A streamed response is sent chunk by chunk as its generator
yields. Session cookies use Flask’s own itsdangerous format, so a server
built here and one running under CPython read each other’s sessions when they
share a secret key.
FastAPI middleware works as in Starlette: @app.middleware("http"),
BaseHTTPMiddleware subclasses, CORSMiddleware, GZipMiddleware and
TrustedHostMiddleware. WebSocket endpoints are supported, including
TestClient.websocket_connect. A WebSocket endpoint runs on a real asyncio
event loop, so it can await asyncio.sleep, tasks and queues while its socket
is open, and pure ASGI middleware sees WebSocket connections as it does in
Starlette. Denying a connection answers with the status the endpoint or
middleware chose, such as 403.
SQLAlchemy
SQLAlchemy’s Core and ORM run over the platform’s SQLite, and on a backend over the PostgreSQL, MySQL and MariaDB drivers the Codename One backend ships. The engine URL picks the database, as it does under CPython:
import os
from sqlalchemy import ForeignKey, String, create_engine, select
from sqlalchemy.orm import (DeclarativeBase, Mapped, Session, mapped_column,
relationship, selectinload)
class Base(DeclarativeBase):
pass
class Author(Base):
__tablename__ = "authors"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(80))
books: Mapped[list["Book"]] = relationship(
back_populates="author", cascade="all, delete-orphan")
class Book(Base):
__tablename__ = "books"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))
author: Mapped[Author] = relationship(back_populates="books")
engine = create_engine(os.environ.get("DATABASE_URL", "sqlite:///library.db"))
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add(Author(name="Ursula", books=[Book(title="The Dispossessed")]))
session.commit()
query = select(Author).options(selectinload(Author.books)).order_by(Author.id)
for author in session.scalars(query):
print(author.name, [b.title for b in author.books])
Declarative models with Mapped annotations, relationships in both
directions (including many-to-many and cascades), selectinload and
joinedload, joins, aliases, subqueries, common table expressions, unions,
single and joined table inheritance and text() queries are supported.
Numeric columns come back as decimal.Decimal and DateTime and Date
columns as datetime values on every database. A text() query has no
column types, so on a server database its numbers and dates come back as
the text the server sent. SQLAlchemy’s own drivers, such as psycopg, aren’t
used: the URL postgresql+psycopg:// selects the backend’s driver.
Stack size
Every request runs on a virtual thread with 1MB of C stack, because transpiled
Python frames are large and re recurses. A handler that recurses deeper than
that raises it with a build hint in codenameone_settings.properties, in bytes
or with a KB or MB suffix:
codename1.arg.python.virtualThreadStack=2MB
CN1_HTTP_VT_STACK can still raise it when the server starts, and the larger
of the two applies.
Compared with CPython, PyPy and GraalPy
The same programs, run on one Apple Silicon Mac with
scripts/python-compare/compare.py and
scripts/python-compare/common_compare.py. Codename One rows are native
executables. The interpreters are CPython 3.14, PyPy 3.11 (7.3.20) and GraalPy
3.13 (Community edition 25.4, on its own JVM). Every program runs in a process
of its own. Memory is the process’s physical footprint, the figure the
operating system meters a process by and the one a memory limit is measured
in, read the same way for every runtime.
One binary, two trade-offs
Every Codename One figure below appears twice, from the same executable.
Codename One is the program on a machine with memory to spare. Its collector adapts to the load. While collecting costs the program little CPU it keeps the heap close to what the program uses. When the program is busy enough that more frequent collections would cost it throughput, it lets the heap grow instead: idle memory on the machine earns nothing, and the speed it buys is real. That’s a choice the runtime makes because the memory is there, not a size it needs.
Codename One, 128 MB budget is the same executable given a memory limit, the way a container, a small cloud instance or a phone would give it one. With a budget, the collector stops spending memory for speed: it keeps the heap tight, and paces allocation so the process stays inside the limit. The budget is a hard cap, not a target: when the limit is near, a thread that needs memory waits for the collector rather than going over. The trade flips, and the program gives back some speed to use less memory.
Collecting garbage has a cost, and at high throughput it shows: under a budget, more of the CPU goes to the collector instead of the program. The server figures below make that visible.
Startup
A program that prints one line, best of ten runs:
| Wall time (ms) | Peak footprint (MB) | |
|---|---|---|
CPython 3.14 | 18.1 | 6.6 |
PyPy 3.11 | 16.5 | 21.9 |
GraalPy 3.13 | 31.9 | 30.0 |
Codename One | 6.1 | 4.5 |
Python kernels
Classic benchmark programs written for Python (scripts/python-bench/kernels),
fastest round in milliseconds, then peak footprint in MB:
| Kernel | CPython 3.14 | PyPy 3.11 | GraalPy 3.13 | Codename One | Codename One, 128 MB budget | |||||
|---|---|---|---|---|---|---|---|---|---|---|
nbody | 783 | 7.1 | 63 | 22.7 | 195 | 126 | 89 | 5.1 | 90 | 5.1 |
richards | 648 | 7.6 | 43 | 24.5 | 113 | 157 | 38 | 5.3 | 36 | 5.4 |
fannkuch | 2680 | 6.9 | 607 | 23.2 | 540 | 138 | 581 | 5.1 | 576 | 5.1 |
spectral_norm | 735 | 6.9 | 12 | 22.5 | 7.5 | 146 | 12 | 5.1 | 12 | 5.1 |
dicts_strings | 390 | 53.5 | 137 | 90.1 | 229 | 294 | 199 | 122 | 219 | 88 |
The numeric kernels allocate almost nothing, so the budget changes nothing
there. dicts_strings builds large dictionaries and strings, and shows the
trade in miniature: under the budget it gives up about a tenth of its speed
for more than a quarter less memory.
A Flask server
The Flask sample above, under thirty seconds of ab -c 8 after a warm-up. Every
server gets the same length of load rather than the same number of requests, so
a fast server isn’t measured before its collector has settled. Footprint is read
when the load ends, again five seconds later, and at its peak. Each server runs
the way app.run() starts it: Werkzeug’s threaded server under CPython, PyPy
and GraalPy, and the native backend server under Codename One, which lowers
the same call onto it.
| First response (ms) | Requests per second | p99 (ms) | Idle (MB) | After load (MB) | 5 s later (MB) | Peak (MB) | |
|---|---|---|---|---|---|---|---|
CPython 3.14 | 115 | 699 | 12 | 28.1 | 28.8 | 28.8 | 29.4 |
PyPy 3.11 | 305 | 692 | 15 | 59.7 | 80.0 | 80.0 | 92.6 |
GraalPy 3.13 | 906 | 559 | 38 | 247.4 | 985.6 | 985.8 | 988.6 |
Codename One | 66 | 27,117 | 2 | 14.0 | 157.2 | 153.2 | 471.9 |
Codename One, 128 MB budget | 27 | 25,619 | 2 | 13.9 | 76.4 | 74.2 | 111.4 |
Why these numbers are suspicious
Read that table with suspicion. Codename One serves 37 times as many requests as CPython, and the gap doesn’t fit anything else on this page. On the kernels above, Codename One runs about as fast as PyPy, and PyPy is many times faster than CPython. Yet here PyPy serves 692 requests a second and CPython 699: the interpreter that runs Python several times faster gains nothing. When a faster engine doesn’t move the result, the engine isn’t what’s being measured.
How the cause was found
Three measurements found what was being measured instead.
First, Flask alone, with no server. scripts/python-compare/flask_cost.py
sends the same request through Flask’s test client, which calls the application
directly without a socket, so it times only the routing, the view and the JSON
response. On one core CPython handles about 14,000 of these requests a second,
PyPy about 28,500 and GraalPy about 5,800. Flask is fast enough on every
interpreter: under app.run(), 95% of each request’s time went somewhere else.
Second, concurrency. With one connection at a time Werkzeug’s server answered 86 requests a second, with 8 connections 686, and with 32 connections 2,707. Throughput rose in step with the number of connections while the server used about a tenth of one core. A server bound by computation stops scaling once its core is busy; this one was mostly waiting.
Third, one request timed by hand: send it, then note when the first byte of the response arrives and when the server closes the connection. The response arrived after 0.4 ms. The server closed the connection about 12 ms after that.
The 12 ms is in werkzeug/serving.py. After every response the development
server drains whatever the client might still be sending, so that a client
whose upload was too large sees the error page rather than a reset
connection. It keeps reading until a select with a 10 ms timeout finds
nothing more, so every connection stays open for at least 10 ms after its
response. ab speaks HTTP/1.0 and counts a request as done when the server
closes the connection. Eight connections that each take about 12 ms come to
about 670 requests a second, whichever interpreter runs the Python. That’s the
number CPython and PyPy both reached.
The Werkzeug documentation is clear that its server is for development, so none of this is a defect. It means the table above compares Codename One’s production server with Python’s development server.
A fair comparison
In production a Flask application runs behind a WSGI server such as gunicorn,
with several worker processes: each worker is a whole interpreter, and they
handle connections in parallel. The rows below serve the same application with
gunicorn -w 8, under the same load. Their footprint is the master process
and all eight workers added together, because that’s what the machine pays
for. GraalPy is missing: gunicorn imports the grp module, which GraalPy
doesn’t provide.
| Requests per second | p99 (ms) | After load (MB) | Peak (MB) | |
|---|---|---|---|---|
CPython 3.14, gunicorn 8 workers | 9,337 | 1 | 254.2 | 254.2 |
PyPy 3.11, gunicorn 8 workers | 9,324 | 2 | 653.2 | 653.2 |
Codename One | 27,117 | 2 | 157.2 | 471.9 |
Codename One, 128 MB budget | 25,619 | 2 | 76.4 | 111.4 |
Behind gunicorn, CPython serves 13 times as many requests as it did under
app.run(). Codename One under its budget still serves 2.7 times as many, in
30% of CPython’s memory and 12% of PyPy’s. That’s the gap to plan around: about
three times the throughput, not 37 times, and a fraction of the memory.
What the fair comparison means
The difference comes from how each server is built, more than from how fast each runs Python:
One process instead of eight. Gunicorn gets its parallelism by running a whole interpreter per worker, and each one costs 30 MB under CPython and more than 70 MB under PyPy. Codename One serves every connection from one process.
The server is native code. Codename One accepts connections, parses requests and writes responses in compiled code, outside the global interpreter lock, on several threads at once. Only the view itself runs as Python, one thread at a time, as under CPython.
Python runs compiled. The view and Flask’s routing run as compiled code, at PyPy’s speed or close to it, from the first request, with no JIT to warm up.
PyPy and CPython reach the same throughput behind gunicorn too, although PyPy runs Flask’s own code about twice as fast, so with eight workers the Python code isn’t the limit for either of them. What PyPy does change is memory: its eight workers, each with its own JIT, use two and a half times CPython’s.
| Deployed size (MB) | |
|---|---|
CPython 3.14 | 82.2 |
PyPy 3.11 | 110.5 |
GraalPy 3.13 | 305.8 |
Codename One | 35.0 |
The deployed size is the interpreter’s installation plus Flask and its dependencies (and gunicorn, under 1 MB), against the one executable.
Where an application needs CPython instead: code that builds source text and
runs it with exec, and packages whose C code is the bulk of the package
rather than an accelerator under its Python, such as scipy or torch.
Replacing those means writing a numerical library in Java, not a few
functions.
Python against Java on Codename One
The workloads below are the ones every Codename One port is measured with,
com.bench.CommonWorkloads, ported line by line to Python in
scripts/python-bench/common. Each one returns a checksum, and every runtime
has to return the checksum Java returns, so all five columns compute the same
thing. The Python port reproduces Java’s 32 and 64-bit wrap-around with masks,
since a Python int never overflows.
The Codename One Java column is the Java original compiled by Codename One into the same executable. It’s the floor: the speed and memory Codename One reaches for this work when written in Java, and the target for the same program written in Python.
| Workload | CPython (ms, MB) | PyPy | GraalPy | Codename One Python | Codename One Java | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| 14452 | 15 | 132 | 32 | 102 | 365 | 66 | 11 | 58 | 11 |
| 11709 | 15 | 5301 | 42 | 11134 | 263 | 39 | 11 | 38 | 11 |
| 1010 | 15 | 186 | 32 | 289 | 354 | 180 | 11 | 167 | 12 |
| 1745 | 382 | 33 | 154 | 26 | 338 | 32 | 42 | 9.3 | 42 |
| 4542 | 199 | 275 | 93 | 234 | 248 | 244 | 26 | 220 | 27 |
| 522 | 15 | 18 | 63 | 58 | 352 | 31 | 164 | 14 | 179 |
| 801 | 15 | 6.1 | 32 | 56 | 331 | 3.5 | 11 | 4.5 | 11 |
| 158 | 23 | 41 | 71 | 87 | 251 | 44 | 27 | 20 | 30 |
| 630 | 15 | 32 | 40 | 67 | 263 | 29 | 84 | 14 | 86 |
| 2152 | 15 | 177 | 57 | 189 | 283 | 86 | 11 | 82 | 11 |
| 1519 | 84 | 168 | 58 | 111 | 247 | 118 | 17 | 80 | 17 |
What the numbers say:
Python reaches Java’s memory. On every workload the compiled Python uses about the memory the Java program uses, and sometimes less: integers, floats and integer lists are held unboxed, a list of integers that fit 32 bits takes four bytes an item as a Java
int[]does, and objects that don’t escape a function are never allocated.And mostly its speed. Six of the eleven run within 15% of Java, among them the arithmetic, recursion and the transcendental math; the rest take 1.5 to 3.4 times as long. What remains is the cost of a Python
intbeing unbounded: where the compiler can’t prove a value stays within 64 bits, the generated code carries the check and a slow path Java doesn’t need.Against the interpreters. Codename One Python is the fastest of the four Python runtimes on six workloads and within 10% of the fastest JIT on three more, without a warm-up; GraalPy is faster at sequential array access and PyPy at allocating objects. It uses a third of PyPy’s memory and a twentieth of GraalPy’s on most of them.
longArithmeticmultiplies 64-bit values whose product needs 128 bits and masks it back: the interpreters compute it with arbitrary precision integers, and the compiler recognizes the wrap-around and computes it in a Javalong.
Tuning a hot spot in Java
A profile usually shows a handful of functions that dominate the run time. Any of them can be rewritten as a Java method in the same project and called from Python like any other Java class. The rest of the program stays Python.
The integer kernel in Python:
M32 = 0xFFFFFFFF
def i32(v: int) -> int:
return ((v + 0x80000000) & M32) - 0x80000000
def int_arithmetic() -> int:
a: int = 0x12345678
b: int = i32(0x9E3779B9)
checksum: int = 0
for _ in range(40000000):
a = i32((a * 1103515245 + 12345) ^ ((b & M32) >> 3))
b = i32((b + a) * 5 - (a << 7))
checksum += (a ^ b) & 0xFFFF
return checksum + a + b
The same function in Java, under common/src/main/java:
package com.codenameone.developerguide.python;
public final class CommonWorkloads {
public static long intArithmetic() {
int a = 0x12345678;
int b = 0x9E3779B9;
long checksum = 0;
for (int i = 0; i < 40000000; i++) {
a = (a * 1103515245 + 12345) ^ (b >>> 3);
b = (b + a) * 5 - (a << 7);
checksum += (a ^ b) & 0xFFFF;
}
return checksum + a + b;
}
}
And the Python that calls it:
from java.com.codenameone.developerguide.python import CommonWorkloads
checksum = CommonWorkloads.intArithmetic()
The Java int wraps by itself, so the masks disappear and the loop compiles
to a few machine instructions. In the run above that took the workload from
1.75 seconds to 65 milliseconds, faster than PyPy and GraalPy, in 11 MB.
Guidelines for choosing what to move:
Measure first. Only a loop that dominates a profile is worth a second language.
Move whole loops, not single operations. Every call across the boundary converts its arguments, so a Java method called once per element gains little; one called once per batch gains the most.
Pass primitive values and Java arrays where possible. A Python
listpassed to Java arrives as ajava.util.Listof boxed values.Annotate the Python that stays. Codename One compiles functions whose parameters and locals have known types (
int,float,bool,str, lists of them and classes withslots) to typed Java code. The rest takes the generic path, which is the one that CPython-like timings come from.