lvgl-python packages LVGL for CPython as the native lvgl extension
distributed under the pydevices-lvgl project name. It is one endpoint of a
shared binding family: it exposes the same intentional Python-shaped API as
the MicroPython and CircuitPython targets, but it does not embed or run a
MicroPython interpreter.
The most important boundary is ownership. This repository owns the CPython
runtime layer, packaging, tests, and release policy. The sibling
lvgl-bindings repository owns the generator and canonical API decisions.
Generated binding files and the fs_driver.py helper are
vendored here at one exact upstream commit; they are inputs to this project,
not ordinary hand-edited source files.
lvgl-bindings (generator and API policy)
|
| explicit sync at a recorded 40-character commit
v
lvgl-python repository
|-- generated/lvgl_python.c and lvgl.pyi generated binding surface
|-- src/lvpy_runtime.c CPython ownership, callbacks, locking
|-- lvgl/ pinned upstream C submodule
`-- setup.py compiles one CPython extension
|
v
import lvgl as lv
|-- generated wrappers convert Python arguments and results
|-- runtime wrappers manage objects, structs, GIL, and callback lifetime
`-- LVGL C core owns widgets, displays, rendering, and event dispatch
Most users install a prebuilt pydevices-lvgl wheel from TestPyPI, then use
import lvgl as lv; they do not clone this repository, initialize its
submodule, or compile LVGL. The build tree exists for binding/runtime
contributors and release automation, not as the normal application setup.
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-lvglAn application may provide its own display backend. In the PyDevices
convenience path it imports display_driver, pydevices' adapter around
appdev.App and board_config. It ships in the pydevices wheel, outside this
one, and is not required for a standalone backend.
| Path | Purpose |
|---|---|
generated/lvgl_python.c |
Vendored generated Python-to-LVGL wrappers, types, constants, and callback entry points. |
generated/lvgl.pyi |
Type stubs installed beside the extension wheel. |
src/lvpy_runtime.c, src/lvpy_runtime.h |
Handwritten CPython-specific implementation: object/struct wrappers, conversion helpers, locking, GIL handling, and callback ownership. |
lvgl/ |
Pinned upstream LVGL C git submodule compiled into the extension. |
lv_conf.h |
LVGL build configuration for the CPython target. |
setup.py |
Defines the lvgl extension and compiles runtime, generated bindings, and all LVGL C sources. |
fs_driver.py |
Synced LVGL filesystem helper; change its canonical copy in lvgl-bindings. |
LVGL_BINDINGS_COMMIT |
Exact 40-character source commit used for the vendored binding inputs. |
tests/ |
Native-extension lifecycle and wheel-installation contracts. |
docs/building.md |
Authoritative local build, sync, Android, and Pyodide instructions. |
docs/publishing.md |
Release safety model and platform-wheel policy. |
scripts/sync_from_lvgl_bindings.sh |
Explicit upstream sync tool; it records the resolved source and updates the submodule pin. |
The usual PyDevices entry point imports display_driver before lvgl. That
adapter selects the active board configuration and wires display flush, input,
and LVGL event-loop coordination; the application then creates widgets.
import display_driver # wires LVGL display, input, and event loop
import lvgl as lv
scr = lv.screen_active()
label = lv.label(scr)
label.set_text("Hello from PyDevices LVGL!")
label.center()- Installing the supported wheel provides the binary module named
lvgl; installingpydevicesprovides thedisplay_driverhelper. - Importing
display_driverfirst establishes the PyDevices display/input integration. It owns LVGL ticking and refresh coordination for this path. - Importing
lvglexposes the generated module surface and runtime support types: LVGL-style enum namespaces, functions, object constructors, and struct methods rather than a separate Python UI model. - Widget constructors and methods enter generated wrappers. They validate and convert Python values, take the runtime's LVGL lock where needed, and call the corresponding LVGL C API.
For a custom backend without PyDevices board configuration, use the raw LVGL setup in the root README instead of adapting this convenience-path example.
The public API is generated, but the lifecycle, ownership, and concurrency
behavior in that path are CPython runtime work. Those concerns belong in
src/lvpy_runtime.*, not in an ad hoc edit to the generated file.
- Do not manually edit
generated/lvgl_python.c,generated/lvgl.pyi,fs_driver.py, orlv_conf.h. Change the generator or canonical helper inlvgl-bindings, regenerate there, then use the explicit sync workflow here. LVGL_BINDINGS_COMMITis a reproducible-source record, not a loose version hint. A sync accepts an immutable full commit SHA or release tag and records the resolved 40-character commit.- A callback registered with
add_event_cbis owned by its registration, not by the widget's Python wrapper:mp_lv_callbackstores it in a dict that LVGL's event descriptor points at, and the runtime drops that dict's one reference when the event is removed or the object deleted. The drop is deferred until no LVGL call or Python callback is in flight, because LVGL sendsLV_EVENT_DELETEto the runtime's hook before the user's. Dropping the wrapper neither stops nor leaks the callback;tests/test_callback_lifetime.pypins each case. - A struct's callbacks (
lv_anim_t,lv_timer_t) share one dict reference, owned by the struct: the generated setters pass the struct ascontaining_struct. A Python-owned struct releases it in its dealloc; a timer and each running copy of an animation release theirs through LVGL'sext_datafree_cb (lvpy_timer_bind,lvpy_anim_bind), which needsLV_USE_EXT_DATA, on for the CPython build only.tests/test_struct_callback_lifetime.pypins each case. - Pair
lv.init()andlv.deinit()in standalone programs. Tests cover this lifecycle, including callback cleanup during interpreter shutdown. - The binding intentionally omits unsafe C signatures such as unbounded pointer-to-pointer or variadic APIs unless conversion is reviewed. Do not restore one merely to make the C surface look complete.
- The project shares naming and selected API policy with other runtimes, but it is a CPython extension. A feature's availability may be explicitly different on another target.
- The extension does not choose a display, event loop, or board. Treat the
PyDevices
display_driverpath as one integration, not as a requirement ofimport lvgl.
Only work on a source checkout when you are changing the generated binding, the CPython runtime, packaging, or the release process. In that case, building.md is the authoritative platform-specific build and test guide; it covers the LVGL submodule and editable native build. It is not needed to use a published wheel.
The focused test suite verifies the recorded bindings source, the installed
lvgl.pyi stub, initialization/deinitialization, widget creation, and
teardown callbacks at interpreter exit. Read publishing.md
before changing sync, version, wheel-matrix, or TestPyPI release behavior:
normal pushes do not publish a wheel.
Good first changes are a focused regression test for a CPython lifecycle or conversion bug, a clarification in the build documentation, or a fix to the handwritten runtime layer when the generated API already expresses the desired surface. Confirm which repository owns the change before editing:
| If the change concerns… | Start in… |
|---|---|
| API availability, generated wrappers, or synced Python helpers | lvgl-bindings |
| CPython object ownership, GIL/lock behavior, wheel packaging, or CPython tests | lvgl-python |
| A PyDevices application's screens, board configuration, or user interaction | that consuming application/repository |
After changing any native code, rebuild the editable extension and run the
focused tests. If a proposed change moves generated files or the lvgl
submodule, use the documented sync path rather than staging a hand-picked
mixture of outputs.
Read src/lvpy_runtime.h first, then follow one call from import lvgl into
generated/lvgl_python.c and back through src/lvpy_runtime.c.
tests/test_lvgl_init.py is short and shows the lifecycle contract the runtime
has to keep.
For the user-facing surface, both setup paths are in the README. For how the binding is generated and which API decisions it makes, go to lvgl-bindings. Building from source is in building.md, and releases are in publishing.md.