Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
79df82e
feat(build): macOS runner intercepts multiprocessing child re-execs
ndonkoHenri Jul 6, 2026
8a06e44
feat(build): Windows runner intercepts multiprocessing child re-execs
ndonkoHenri Jul 6, 2026
955fbbe
feat(build): Linux runner intercepts multiprocessing child re-execs
ndonkoHenri Jul 6, 2026
ba85721
fix(build): run the app module as the real __main__; point multiproce…
ndonkoHenri Jul 6, 2026
c485b71
feat(create): guard ft.run() with __main__ in the app scaffold
ndonkoHenri Jul 6, 2026
eef9908
refactor(build): splice all dynamic boot-script values via jsonEncode
ndonkoHenri Jul 6, 2026
a0ed920
docs: Multiprocessing cookbook recipe
ndonkoHenri Jul 7, 2026
6cf2905
add changelog entry
ndonkoHenri Jul 7, 2026
35894b6
Merge branch 'flet-0.86' into multiprocessing
ndonkoHenri Jul 7, 2026
596b497
fix(build): open console.log as UTF-8
ndonkoHenri Jul 7, 2026
6c65a66
fix: hide the console window of the git version-fallback on Windows
ndonkoHenri Jul 7, 2026
e7187d6
add cookbook recipe and examples
ndonkoHenri Jul 8, 2026
1522f5d
docs: add detailed usage guides to CLI command docstrings
ndonkoHenri Jul 8, 2026
09dc4bf
docs: update README of build-template with installation, platform sup…
ndonkoHenri Jul 8, 2026
876a006
breaking changes in version folders
ndonkoHenri Jul 8, 2026
07687fa
update changelog
ndonkoHenri Jul 8, 2026
132324c
Merge branch 'flet-0.86' into multiprocessing
ndonkoHenri Jul 8, 2026
2aeb08a
fix(build): splice {module_name} into the boot script via jsonEncode too
ndonkoHenri Jul 8, 2026
66f6dec
docs: drop leftover debug prints from the persistent-worker example
ndonkoHenri Jul 8, 2026
c44b7ed
docs: fix relative links broken by the breaking-changes version folders
ndonkoHenri Jul 8, 2026
42b5268
Bump python build release and serious_python
FeodorFitsner Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### New features

* Add support for **Python `multiprocessing` in packaged Flet desktop apps** built with `flet build macos`, `flet build windows`, and `flet build linux`. `multiprocessing` APIs such as `Process`, `ProcessPoolExecutor`, the `spawn`/`forkserver` start methods, and the resource tracker now work in packaged desktop apps. Previously, worker processes re-executed `sys.executable`, which pointed to the app binary itself, causing each worker to launch another GUI app instance and hang. Flet desktop runners now detect CPython multiprocessing child command lines before Flutter starts and divert them to a headless embedded Python interpreter via `dart_bridge` 1.5.0+. The Python bootstrap also runs the app module as the real `sys.modules["__main__"]` with `python -m` semantics, so top-level worker functions in `main.py` can be pickled correctly. When using `multiprocessing`, your app must follow normal Python multiprocessing rules: guard `ft.run(...)` with `if __name__ == "__main__":`, define worker functions at module top level, and do not access Flet UI objects from worker processes. See the new [Multiprocessing](https://docs.flet.dev/cookbook/multiprocessing) cookbook page. Mobile platforms remain unsupported because iOS and Android do not allow apps to spawn arbitrary child processes ([#4283](https://github.com/flet-dev/flet/issues/4283), [#6577](https://github.com/flet-dev/flet/pull/6662)) by @ndonkoHenri.
* Multi-version bundled CPython support in `flet build` and `flet publish`. Pick the runtime your app ships with via the new `--python-version` flag (3.12 / 3.13 / 3.14), or let it be derived from `[project].requires-python` in your `pyproject.toml`; defaults to the latest supported stable (currently 3.14). The matching CPython-standalone build, Pyodide release (0.27.7 / 0.29.4 / 314.0.1), and Emscripten wheel platform tag are all resolved from `flet-dev/python-build`'s date-keyed manifest. Adding a future pre-release CPython line (e.g. 3.15 beta) is a one-row append with `prerelease=True` — opt-in only via an explicit `--python-version 3.15` or `requires-python = "==3.15.*"`, never the auto-resolved default. Requires `serious_python` >= 4.0.0, now pinned in the `flet build` template. See the new [Choosing a Python version](https://flet.dev/docs/publish#choosing-a-python-version) docs section ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner.
* Add `ft.DataChannel`: dedicated byte channels for widgets that move bulk binary data (image frames, audio buffers, ML tensors) between Dart and Python, bypassing the MsgPack control protocol. The Dart side opens a channel via `FletBackend.of(context).openDataChannel()` and announces it to Python by firing a `data_channel_open` control event with `{channel_name, channel_id}`; the Python side declares `on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]]` and captures the channel via `self.get_data_channel(e.channel_id)`. Backed by a dedicated `PythonBridge` per channel in embedded native mode (4–7 GiB/s on M2 Pro) and by the default `ProtocolMuxedDataChannelFactory` in dev / web modes (raw-byte frames muxed over the active Flet protocol transport with a 1-byte type discriminator). Pyodide gets zero-copy outbound sends via `postMessage` Transferable ArrayBuffer. First consumer: `flet-charts` `MatplotlibChartCanvas`, migrated from `_invoke_method` PNG dispatch to a 1-byte-opcode data channel by @FeodorFitsner.
* **In-process Python transport (`dart_bridge` FFI).** `package:flet` gains a third protocol transport alongside the UDS / TCP socket servers: it can run Flet's MsgPack protocol over an in-process `dart_bridge` byte channel via a `FletApp(channelBuilder: …)` seam (the `flet` package stays Python-independent — it doesn't depend on `serious_python` or know about `PythonBridge`; the embedder wires the channel). `serious_python` >= 3.0.0 uses this seam to embed the Python interpreter **in-process** instead of talking to it over a localhost socket, and the `flet build` template migrates from sockets to the FFI transport. On Android, where the OS may keep the process alive across a back-button quit and restart only the Dart VM, the transport rebinds to the new VM's `dart_bridge` ports on a session-restart signal (`libdart_bridge` >= 1.3.0) — the Python process and its in-memory state are preserved and the Flet session is rebuilt from `REGISTER_CLIENT` by @FeodorFitsner.
Expand All @@ -24,11 +25,11 @@

### Breaking changes

* **App files now ship unpacked in a read-only bundle, and the storage directories were reworked** (requires `serious_python` >= 4.0.0, now pinned in the `flet build` template). Your Python sources ship **unpacked inside the app bundle** next to the stdlib/site-packages (no first-launch `app.zip` extraction) on macOS/iOS/Windows/Linux; on Android they ship as a *stored* `app.zip` asset unpacked once on first launch; web is unchanged. The app directory is now **read-only**, so the Python program's **working directory** moved to a writable, app-private data dir. `FLET_APP_STORAGE_DATA` now maps to the OS *application support* dir (a `data` subdir) instead of the user's Documents folder and is the cwd; `FLET_APP_STORAGE_TEMP` now points to the OS temp dir (was the cache dir) and a new `FLET_APP_STORAGE_CACHE` exposes the cache dir. `flet run` sets the dev cwd to a hidden, git-ignored `<project>/.flet/storage/data`. Relative **reads** of bundled files (`open("seed.json")`) must move to `__file__`/`importlib.resources` or `assets/`. See the [app files unpacked / storage dirs](/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle) guide by @FeodorFitsner.
* **App files now ship unpacked in a read-only bundle, and the storage directories were reworked** (requires `serious_python` >= 4.0.0, now pinned in the `flet build` template). Your Python sources ship **unpacked inside the app bundle** next to the stdlib/site-packages (no first-launch `app.zip` extraction) on macOS/iOS/Windows/Linux; on Android they ship as a *stored* `app.zip` asset unpacked once on first launch; web is unchanged. The app directory is now **read-only**, so the Python program's **working directory** moved to a writable, app-private data dir. `FLET_APP_STORAGE_DATA` now maps to the OS *application support* dir (a `data` subdir) instead of the user's Documents folder and is the cwd; `FLET_APP_STORAGE_TEMP` now points to the OS temp dir (was the cache dir) and a new `FLET_APP_STORAGE_CACHE` exposes the cache dir. `flet run` sets the dev cwd to a hidden, git-ignored `<project>/.flet/storage/data`. Relative **reads** of bundled files (`open("seed.json")`) must move to `__file__`/`importlib.resources` or `assets/`. See the [app files unpacked / storage dirs](/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle) guide by @FeodorFitsner.
* `flet build` and `flet publish` now bundle CPython 3.14 by default (previously 3.12, implicit via the old single-version `serious_python`). Existing apps that depend on native wheels without 3.14 binaries should pin explicitly with `--python-version 3.12` (CLI), `requires-python = ">=3.12,<3.13"` (pyproject), or `SERIOUS_PYTHON_VERSION=3.12` in the build environment ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner.
* Android builds now include only the ABIs the bundled Python supports, sourced per-version from python-build's manifest (`pythons.<short>.android_abis`) rather than hardcoded in flet. As of python-build `20260630`, `armeabi-v7a` (32-bit ARM) is published for **3.12, 3.13 and 3.14**, so all three build it by default; an explicit `--arch <abi>` is validated against the selected Python's supported set and fails with a clear error otherwise ([#6578](https://github.com/flet-dev/flet/pull/6578)) by @ndonkoHenri.
* `flet build` / `flet publish` now **compile your app and packages to `.pyc` by default** (previously off). This removes per-launch bytecode recompilation — a significant cold-start win, especially on mobile where pure Python is imported from a stored zip (`zipimport`) and can't cache bytecode back to disk, so every module would otherwise recompile from source on each launch. The CLI flags gain `--no-compile-app` / `--no-compile-packages` (via `argparse.BooleanOptionalAction`; the existing `--compile-app` / `--compile-packages` still work), and `[tool.flet.compile].app` / `.packages` now default to `true`. Pass `--no-compile-*` or set them to `false` to restore the old behavior (faster iterative builds, or keeping `.py` source in the bundle). Compiled web builds were verified to load in Pyodide (bundled CPython and Pyodide share the same minor version). See the [compile-on-by-default](/docs/updates/breaking-changes/v0-86-0-compile-on-by-default) guide ([#6598](https://github.com/flet-dev/flet/pull/6598)) by @FeodorFitsner.
* Flet protocol wire format on stream-oriented transports (UDS / TCP) is incompatible with pre-0.86 servers and clients. Every packet now starts with a 4-byte little-endian length prefix and a 1-byte type discriminator (`0x00` = MsgPack control frame, `0x01` = raw DataChannel frame). WebSocket / `postMessage` / `dart_bridge` transports keep native message boundaries and only gain the type byte. The Flet CLI dev server and the in-process Python runtime are upgraded in lockstep — running `flet run` with mismatched `flet` versions across CLI and runtime is no longer supported. See the [DataChannel protocol framing upgrade](/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade) guide. The `MatplotlibChartCanvas` widget transports its full / diff / clear frames via a `DataChannel` rather than `_invoke_method` arguments — visually identical, but custom code that subclassed it and overrode the apply methods may need updating by @FeodorFitsner.
* `flet build` / `flet publish` now **compile your app and packages to `.pyc` by default** (previously off). This removes per-launch bytecode recompilation — a significant cold-start win, especially on mobile where pure Python is imported from a stored zip (`zipimport`) and can't cache bytecode back to disk, so every module would otherwise recompile from source on each launch. The CLI flags gain `--no-compile-app` / `--no-compile-packages` (via `argparse.BooleanOptionalAction`; the existing `--compile-app` / `--compile-packages` still work), and `[tool.flet.compile].app` / `.packages` now default to `true`. Pass `--no-compile-*` or set them to `false` to restore the old behavior (faster iterative builds, or keeping `.py` source in the bundle). Compiled web builds were verified to load in Pyodide (bundled CPython and Pyodide share the same minor version). See the [compile-on-by-default](/docs/updates/breaking-changes/v0-86-0/compile-on-by-default) guide ([#6598](https://github.com/flet-dev/flet/pull/6598)) by @FeodorFitsner.
* Flet protocol wire format on stream-oriented transports (UDS / TCP) is incompatible with pre-0.86 servers and clients. Every packet now starts with a 4-byte little-endian length prefix and a 1-byte type discriminator (`0x00` = MsgPack control frame, `0x01` = raw DataChannel frame). WebSocket / `postMessage` / `dart_bridge` transports keep native message boundaries and only gain the type byte. The Flet CLI dev server and the in-process Python runtime are upgraded in lockstep — running `flet run` with mismatched `flet` versions across CLI and runtime is no longer supported. See the [DataChannel protocol framing upgrade](/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade) guide. The `MatplotlibChartCanvas` widget transports its full / diff / clear frames via a `DataChannel` rather than `_invoke_method` arguments — visually identical, but custom code that subclassed it and overrode the apply methods may need updating by @FeodorFitsner.

### Deprecations

Expand Down
69 changes: 69 additions & 0 deletions sdk/python/examples/cookbook/multiprocessing/cancel_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import multiprocessing
import time

import flet as ft


def long_job(seconds: int) -> None:
"""Burn CPU for roughly `seconds` seconds.

Stands in for work you cannot interrupt from Python itself, e.g. a tight
loop inside a C extension library. Runs in a worker process, which is why
it can be cancelled at any point — threads can't be stopped like that.
"""
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
sum(range(1_000_000))


def main(page: ft.Page):
process = None

def watch(p: multiprocessing.Process):
"""Wait (on a background thread) for the worker to end, then report
whether it finished on its own or was cancelled."""
p.join()
if p.exitcode == 0:
status.value = "Process finished normally."
else:
# terminate() ends the child with a negative exit code (-SIGTERM).
status.value = f"Process was terminated (exit code {p.exitcode})."
start.disabled = False
cancel.disabled = True
page.update()

def start_job():
nonlocal process
process = multiprocessing.Process(target=long_job, args=(30,), daemon=True)
process.start()
status.value = f"Running in process {process.pid}…"
start.disabled = True
cancel.disabled = False
page.update()
page.run_thread(watch, process)

def cancel_job():
if process is not None and process.is_alive():
process.terminate() # threads can't do this

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Row(
controls=[
start := ft.Button("Start 30s job", on_click=start_job),
cancel := ft.Button(
"Cancel", on_click=cancel_job, disabled=True
),
]
),
status := ft.Text(),
]
)
)
)


if __name__ == "__main__":
ft.run(main)
46 changes: 46 additions & 0 deletions sdk/python/examples/cookbook/multiprocessing/parallel_sort.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import random
from concurrent.futures import ProcessPoolExecutor, as_completed

import flet as ft


def sort_chunk(chunk: list[float]) -> list[float]:
"""Sort one chunk of data."""
return sorted(chunk)


def main(page: ft.Page):
def run_sort():
"""Orchestrate the pool from a background thread, updating the UI
from the main process as results come in."""
chunks = [[random.random() for _ in range(250_000)] for _ in range(8)]
completed = 0
# Without max_workers, the pool sizes itself to the machine's CPUs.
with ProcessPoolExecutor() as pool:
futures = [pool.submit(sort_chunk, c) for c in chunks]
# as_completed() yields each future as soon as its worker is done.
for _ in as_completed(futures):
completed += 1
progress.value = completed / len(futures)
status.value = f"Sorted {completed}/{len(futures)} chunks"
page.update()
status.value = "Done!"
page.update()

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Button(
"Start sorting", on_click=lambda: page.run_thread(run_sort)
Comment thread
ndonkoHenri marked this conversation as resolved.
),
status := ft.Text("Idle"),
progress := ft.ProgressBar(value=0, width=300),
]
)
)
)


if __name__ == "__main__":
ft.run(main)
65 changes: 65 additions & 0 deletions sdk/python/examples/cookbook/multiprocessing/persistent_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import multiprocessing
import time

import flet as ft


def calc_worker(conn) -> None:
"""Serve calculation jobs over the pipe until told to stop.

Runs in a worker process that stays alive across jobs, so the process
startup cost is paid once. Expensive setup (loading models, opening
datasets, warming caches) could be done once here, before the loop,
and reused by every job. Receiving `None` is the shutdown signal.
"""
while (job := conn.recv()) is not None:
started = time.perf_counter()
result = sum(i * i for i in range(job))
conn.send((result, time.perf_counter() - started))
conn.close()


def main(page: ft.Page):
# One end of the pipe goes to the worker, the other stays with the UI.
parent_conn, child_conn = multiprocessing.Pipe()
worker = multiprocessing.Process(
target=calc_worker, args=(child_conn,), daemon=True
)
worker.start()

def submit():
# One job in flight at a time: the button stays disabled until the
# worker replies (a bare Pipe is not multiplexed).
button.disabled = True
status.value = "Working…"
page.update()
page.run_thread(request)

def request():
"""Send a job to the worker and wait for its reply.

Should be run on a background thread: conn.recv() blocks until the worker
answers, so it must stay off the UI event loop.
"""
parent_conn.send(100_000_000)
result, duration = parent_conn.recv()
status.value = (
f"sum of squares = {result}\n{duration:.2f}s in process {worker.pid}"
)
button.disabled = False
page.update()

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
button := ft.Button("Compute in worker", on_click=submit),
status := ft.Text(f"Worker ready (pid {worker.pid})"),
]
)
)
)


if __name__ == "__main__":
ft.run(main)
58 changes: 58 additions & 0 deletions sdk/python/examples/cookbook/multiprocessing/worker_progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import multiprocessing

import flet as ft


def crunch(progress_queue: multiprocessing.Queue, steps: int) -> None:
"""Do `steps` slices of CPU-heavy work, reporting progress after each one.

Runs in a worker process, which has no access to the page — the queue is
the only channel back to the UI. Values are fractions 0..1; a final `None`
tells the consumer there is nothing more to read.
"""
for i in range(steps):
sum(range(50_000_000)) # one slice of real work
progress_queue.put((i + 1) / steps)
progress_queue.put(None) # sentinel: no more updates


def main(page: ft.Page):
def start():
button.disabled = True
status.value = "Starting worker…"
page.update()
queue = multiprocessing.Queue()
worker = multiprocessing.Process(target=crunch, args=(queue, 20), daemon=True)
worker.start()
page.run_thread(drain, queue, worker)

def drain(queue: multiprocessing.Queue, worker: multiprocessing.Process):
"""Forward the worker's progress reports to the UI.

Runs on a background thread: queue.get() blocks until the worker
reports again, so it must stay off the UI event loop.
"""
while (value := queue.get()) is not None:
progress.value = value
status.value = f"Crunching… {value:.0%}"
page.update()
worker.join()
status.value = "Done!"
button.disabled = False
page.update()

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
button := ft.Button("Start", on_click=start),
progress := ft.ProgressBar(value=0, width=300),
status := ft.Text(),
]
)
)
)


if __name__ == "__main__":
ft.run(main)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Command(BaseBuildCommand):
Android (APK/AAB), and iOS (IPA and simulator .app), with a wide range of
customization options for metadata, assets, splash screens, and signing.
Detailed guide with usage examples: https://flet.dev/docs/publish
Detailed usage guide: https://flet.dev/docs/publish
"""

def __init__(self, parser: argparse.ArgumentParser) -> None:
Expand Down
Loading
Loading