Native desktop APIs — windows, tray icons, menus, displays, global shortcuts, dialogs, preferences and secure storage — for Python 3.10+, on macOS, Windows and Linux.
Pure ctypes over the libnativeapi C ABI:
no compiled extension module, so one wheel per platform serves every Python 3
version. The typed Python layer is generated from the C++ headers of core by
./codegen; only nativeapi/_library.py, nativeapi/_runtime.py and the
event loop shim in src/ are hand-written.
Status: prototype. The whole API surface is generated, but it has not been published and the API may still change.
import asyncio
from nativeapi import Application, Size, Window, WindowClosedEvent, WindowManager
async def main() -> int:
window = Window()
window.set_title("Hello")
window.set_size(Size(800, 600), False)
window.center()
def on_event(event):
if isinstance(event, WindowClosedEvent):
Application.quit()
WindowManager.add_listener(on_event)
return await Application.run_async(window) # asyncio keeps running
asyncio.run(main())- Objects (
Window,Menu,TrayIcon, …) wrap a handle. The default constructor isWindow(); other constructors are class methods (Preferences.with_scope("app")). A failed creation raisesNativeApiError. The reference is released bydispose()/with, or when the wrapper is garbage collected. Calls on a released handle fail safely. - Getters without arguments are properties (
window.title,window.bounds,window.is_visible). One with a matching one-argument setter is writable too (window.title = "Hello"); theset_title(...)method stays, and setters that need more than the value (window.set_size(size, animate)) are methods only. - Values (
Point,Size,Rectangle,Color, …) are dataclasses. - Enums are
IntEnums (TitleBarStyle.HIDDEN); bit sets areIntFlags (ModifierKey.SHIFT | ModifierKey.ALT). - Events are frozen dataclasses, one subclass per kind, so they work
with
match:case WindowMovedEvent(window_id=id, new_position=p). - Singletons (
Application,WindowManager,DisplayManager, …) are classes with static methods.
Application.run(window=None)blocks in the platform loop and returns the exit code. Listeners run on the main thread in between. Ctrl+C terminates the process.await Application.run_async(window=None)pumps the platform loop from the running asyncio loop instead, so tasks, timers and I/O keep running while windows are up. It resolves with the exit code onceApplication.quit(code)is called. An exception raised in a listener goes to the asyncio loop's exception handler.
Both must be called on the main thread.
cd examples/python_window_example && uv run main.py # builds the wheel, runs the exampleFor work on the binding itself, build the library in place and run from the
source tree; nativeapi/_library.py finds it in build/:
cmake -S bindings/python -B bindings/python/build && cmake --build bindings/python/build
cd bindings/python && PYTHONPATH=. uvx --with pytest pytestNATIVEAPI_LIBRARY=/path/to/libnativeapi.dylib overrides the lookup.
Development happens in nativeapi, which holds every binding and the code generator and checks out the core library as a submodule:
git clone --recursive https://github.com/libnativeapi/nativeapi.gitFiles marked AUTO-GENERATED. DO NOT EDIT. are generated from the C++ headers in nativeapi. To change the API, send a pull request there; maintainers regenerate the bindings.
- API requests and native behavior bugs → nativeapi-core issues
- Bugs specific to one binding → nativeapi issues
- Not sure → nativeapi-core issues