Serious Python

repository·main·Indexed 43 days ago

https://github.com/flet-dev/serious-python

A Flutter plugin that embeds a full Python runtime into mobile and desktop applications. It enables running complex Python logic—such as ML, AI, and heavy processing—in the background of a Flutter app, communicating via REST APIs, sockets, files, or the high-performance PythonBridge byte transport mechanism. Supports iOS, Android, macOS (Darwin), Linux, Windows, and Web (via Pyodide) with configurable Python versions (e.g., 3.12, 3.13, 3.14).

Tokens
20.5K
Snippets
60
Records
88
Agent score
50%

What's inside Serious Python

  1. Overview of Serious Python

    main

    Serious Python is a cross-platform Flutter plugin that embeds a Python runtime into mobile or desktop applications. It allows you to run Python programs in a background thread to perform heavy lifting tasks like image processing, ML, AI, or managing SQLite databases without blocking the Flutter UI.

    Key Architectural Concept: Flutter does not call Python functions directly. Instead, the Flutter app communicates with the Python environment via an API provided by the Python program itself, such as a REST API, sockets, files, or a shared SQLite database.

  2. Handle DataChannel concurrency on the Python side

    main

    The channel.on_bytes(handler) method executes the handler synchronously under the Python GIL on the transport's delivery thread. Heavy processing inside this handler will starve the Python runtime.

    Recommended Pattern: Use a bounded queue and a worker pool to offload processing from the delivery thread. This prevents the transport from blocking and allows you to implement a 'drop' policy for overflow using queue.put_nowait.

    Note on Parallelism:

    • Real parallelism requires C extensions that release the GIL (e.g., NumPy, PyTorch, Pillow).
    • For pure-Python CPU work, use multiprocessing.
    • Warning: multiprocessing is only available on desktop hosts in embedded mode. It is not available on iOS/Android due to OS restrictions on spawning child processes.
    def did_mount(self):
        self._queue = queue.Queue(maxsize=4)         # bounded queue for drop policy
        self._pool = ThreadPoolExecutor(max_workers=4)
        self._frames.on_bytes(self._enqueue)
        threading.Thread(target=self._pump, daemon=True).start()
    
    def _enqueue(self, payload):
        try:
            self._queue.put_nowait(payload)
        except queue.Full:
            pass                       # drop overflow to prevent pile-up
    
    def _pump(self):
        while True:
            payload = self._queue.get()
            self._pool.submit(self._decode_and_reply, payload)
  3. How Framework Discovery Works via .fwork and .origin files

    main

    Because binaries cannot reside directly on sys.path as .so files on iOS, serious-python uses a specialized loader that relies on two marker files to bridge the gap between the module's logical location and its physical location in the Frameworks folder:

    1. .fwork file: Acts as a placeholder in the original location of the binary on sys.path. It contains the path to the actual binary in the Frameworks folder, relative to the app bundle.
    2. .origin file: Located inside the .framework directory. It contains the path to the .fwork file, relative to the app bundle, allowing the system to resolve the framework back to its original location.

    Module Metadata Behavior:

    • When a module is loaded using this mechanism, the module's __file__ attribute reports the location of the .fwork file. This allows developers to use __file__ as an anchor for filesystem traversal.
    • The __spec__.origin will reference the location of the actual binary inside the .framework folder.
  4. Use Dedicated Data Channels for high-throughput data

    main

    Flet's standard control protocol uses MsgPack, which is efficient for widget state but slow for bulk payloads (images, audio, video) due to serialization overhead.

    DataChannel provides a dedicated byte channel that bypasses MsgPack, allowing for much higher throughput (up to 7 GiB/s in embedded native mode). Use this for:

    • Sending large bitmaps/frames from Python to a Dart widget.
    • Streaming camera frames from Dart to Python for ML inference.
    • Streaming PCM audio from a microphone to a Python pipeline.
    # Python side: sending raw bytes
    self._frames.send(rgba_bytes)
    
    # Dart side: receiving raw bytes
    _frames.messages.listen((Uint8List bytes) => ...);
  5. Manage DataChannel concurrency in Dart Isolates

    main

    When working with Flutter/Dart, understand the scope of the PythonBridge and FletBackend:

    • Main Isolate: FletBackend.of(context).openDataChannel() is only available on the main Isolate because it requires BuildContext.
    • Worker Isolates: To use parallelism in a worker Isolate (e.g., for video decoding), the worker must construct its own PythonBridge directly by importing package:serious_python/bridge.dart. It can then ship its RawReceivePort back to the main Isolate via a SendPort to trigger data_channel_open events.
    OperationSafe?
    FletBackend.of(context).openDataChannel() from a worker Isolate✗ (BuildContext is main-Isolate only)
    Multiple PythonBridge instances across Isolates✓ (libdart_bridge multiplexes)
    One PythonBridge instance used from multiple Isolates✗ (RawReceivePort is single-Isolate)
    Python dart_bridge.send_bytes(port, ...) from worker threads✓ (serialised internally)
    Python handler running concurrently for one port✗ (GIL limits to one in-flight handler per port)
  6. Ensure Python version consistency during build

    main

    When building an app, you must ensure that the Python version used to install packages matches the Python runtime bundled in the Flutter app.

    The Correct Way: Set the SERIOUS_PYTHON_VERSION environment variable. This variable is read by both the package command (to install the correct site-packages) and by the native Flutter build scripts (to bundle the correct runtime).

    The Incorrect Way: Do not rely solely on the --python-version flag in the package command. This flag only affects the packaging step and does not reach the later flutter build phase, which leads to a mismatched app (e.g., 3.13 packages bundled with a 3.14 runtime).

    Note: If you change the Python version, perform a clean build (flutter clean) to avoid using stale compiled bytecode.

    # Correct: Set the env var to ensure both phases use 3.13
    export SERIOUS_PYTHON_VERSION=3.13
    dart run serious_python:main package app/src -p Android
    
    # Then run your flutter build as usual
    flutter build apk
  7. Design the communication between Flutter and Python

    main

    By default, the embedded Python program runs in a separate thread to prevent blocking the Flutter UI.

    Important: Your Flutter app should not attempt to call Python functions or modules directly. Instead, establish communication via standard APIs, such as:

    • REST APIs
    • Sockets
    • SQLite databases
    • File system operations

    To keep the Python program running in the background, it must be blocking. Examples include:

    • Running a Flask app listening on a specific port (e.g., 8000).
    • Using threading.Thread for long-running computations combined with threading.Event to prevent the main program from exiting.

    If you need to run a short utility program synchronously, use the sync: true parameter in the SeriousPython.run() method. Note that synchronous execution can block the UI unless run in a Dart isolate.

    // Example concept: Python running a blocking Flask server
    # app.py
    from flask import Flask
    app = Flask(__name__)
    
    @app.route('/')
    def hello():
        return 'Hello from Python!'
    
    if __name__ == '__main__':
        app.run(port=8000)
  8. iOS Binary Module Requirements for App Store Compatibility

    main

    To comply with iOS App Store requirements, all binary modules in an iOS app must be packaged as dynamic libraries within a framework.

    Key constraints:

    • All binaries must be contained in a framework with appropriate metadata.
    • Frameworks must be stored in the Frameworks folder of the packaged app.
    • There can be only one binary per framework.
    • No executable binary material is allowed outside the Frameworks folder.
    • Extension modules are not packaged as .so files on sys.path; instead, they are packaged as standalone frameworks and discovered via .fwork placeholder files.
  9. Understand the PythonBridge concept

    main

    The PythonBridge is an in-process byte transport mechanism between the Dart runtime and the embedded CPython runtime. It is designed for high-performance communication without the overhead of a protocol layer like MsgPack or Flet's messaging system.

    To optimize performance, the bridge uses separate channels for different types of traffic:

    1. Control Channel: Used for interactivity and small messages (e.g., counters, version checks, memory snapshots). It uses UTF-8 JSON with a {"op": ...} request format and a {"event": ...} response format.
    2. Echo Channel: Used for high-throughput data transfer (e.g., moving large files or images). It uses raw bytes, where Python echoes the received frame verbatim to minimize framing tax.

    Separating these channels ensures that the high-throughput path remains a simple bridge.send → dart_bridge.send_bytes loop, while the JSON dispatcher only handles lightweight control messages.

  10. Configure Android requirements for serious_python

    main

    When targeting Android, ensure your minSdk is set to at least 23 in your android/app/build.gradle.kts. This allows serious_python to keep native libraries uncompressed and page-aligned in the APK, enabling them to be memory-mapped directly from the APK without extraction.

    If you have packages that rely on file system paths (using __file__ or pkg_resources) rather than importlib.resources, they may fail when running from a zip. To fix this, set the SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES environment variable to a comma-separated list of relative package paths to force them to be extracted to disk instead of being read from the zip.

    // android/app/build.gradle.kts
    android {
        defaultConfig {
            minSdk = 23
        }
    }
  11. Configure Android requirements for native Python modules

    main

    When targeting Android, serious_python handles Python extension modules by relocating them into jniLibs and loading them directly from the APK via memory-mapping. To ensure native libraries remain uncompressed and page-aligned in the APK (which is required for this loading mechanism), you must set the minSdk to at least 23 in your Android configuration.

    android {
        defaultConfig {
            minSdk = 23
        }
    }
  12. Implement the Xcode Build Step for iOS Binary Packaging

    main

    The Xcode project responsible for building the app must include a build step that automates the conversion of .so files into iOS-compliant frameworks.

    The build step must perform the following tasks:

    1. Locate any .so files within the PYTHONPATH.
    2. Convert each .so file into a standalone framework in the Frameworks folder.
    3. Strip extensions from the module file names.
    4. Add necessary framework metadata (including an Info.plist).
    5. Sign the resulting framework.
    6. Create the .fwork marker files in the original module locations.
    7. Create the .origin files inside each .framework directory.