#6839·flet

`flet build` app packaging: incomplete default exclusions, and no way to see what was dropped

Author: ndonkoHenriCreated Sep 12, 2026Updated Sep 12, 2026

Summary

flet build copies the app directory into the package with a single default exclusion — build — so a project whose app root is the project root ships its virtualenv, .git, editor directories, stray files and previous build output to end users. Separately, what is dropped happens across four phases with two different matchers, and none of it is reported or documented, so a user cannot find out what Flet actually removed from their app.

Two halves: a concrete defect (part 1) and a design question for discussion (part 2).


Part 1 — the defect

build_base.py:2619:

python
exclude_list = ["build"]
# + user's --exclude / [tool.flet.app.exclude] / [tool.flet.<platform>.app.exclude]
# + "assets" when target_platform == "web"

That is the entire filter. Nothing excludes .venv, venv, .git, .flet, .idea, .vscode, .mypy_cache, .ruff_cache or .pytest_cache.

Measured — web

A minimal app (one main.py, two small assets, flet + flet-lottie) with a .venv at the project root — the layout uv init and python -m venv .venv produce:

build/web/assets/app/app.zip 39,963,366 bytes
uncompressed 95.17 MB
.venv share 2985 of 3384 entries (92.63 MB)
inside .venv 1793 .pyc, 0 .py

The last row matters: 1793 .pyc and zero .py means compileall walked the entire virtualenv before it was packaged. The venv is compiled and shipped.

Measured — macOS (same project, flet build macos)

.app bundle 310 MB
app payload inside the bundle 184 MB
.venv 119 MB
a stray previous build output 65 MB

The full packaged payload was .flet .keep .venv assets build-macos.log build.log main.pyc pyproject.toml quiet_app.pyc runweb.log runweb2.log runweb3.log server-mode-client t.log v.log v1.log. Everything the app actually needs — main.pyc, pyproject.toml, assets/ — is a rounding error against that. Note .flet/ in there: the dev-storage directory flet run creates, i.e. the development database and cache, shipped to end users.

Scope

Every platform, and the delivery model differs per platform without changing the outcome. Per the 0.86 guide App files ship unpacked in a read-only bundle:

  • macOS / iOS / Windows / Linux — app files ship unpacked inside the bundle, next to the stdlib and site-packages. No app.zip, no first-launch extraction. Confirmed above: .venv sits unpacked at Contents/Resources/serious_python_darwin_*.bundle/Contents/Resources/app/.venv.
  • Android — the app ships as a stored (uncompressed) app.zip asset inside the APK and is unpacked once, version-keyed, to the app's files directory on the first launch after an install or update. So on Android the bytes are paid for twice: in the download and again on device.
  • Web — unchanged; app.zip is fetched by the browser before the app starts.

All 8 flet build targets go through package_python_app(), as do flet debug and flet test. The only platform-conditional line in the exclusion list is assets on web.

Who is affected

flet create is immune: its template sets [tool.flet.app] path = "src", so a project-root .venv sits above the packaged directory.

It hits projects whose app root is the project root — hand-rolled layouts, and 536 of 538 example pyproject.toml files in this repo. No CI covers it.

Prior reports, both closed without changing the default:

  • #5983 — 2.8 GB / 17,755 files, down to 261 MB / 82 files once the venv was excluded
  • #6135 — 189 MB APK whose 127 MB app.zip contained .venv, .venv mobile, .venv desktop and .idea

flet publish already avoids this by a different route: filter_tar drops every top-level dot-entry (publish.py:299-309), so .venv never reaches its output. It has the mirror-image hole instead — it never drops build/. The two packagers have complementary blind spots.

The docs currently recommend the workaround rather than shipping the default (website/docs/publish/index.md:1444, and the canonical example is flet build <target_platform> --exclude .git .venv).

Proposed fix

Detect virtual environments structurally rather than by name: scan the app root for directories containing pyvenv.cfg (PEP 405 — written by python -m venv, uv venv and virtualenv alike; verified for all three, with a decoy package named env correctly left alone). Add a small name list for entries that are tool-reserved and have no marker: .git, .flet, .idea, .vscode, .mypy_cache, .ruff_cache, .pytest_cache.

Merge with the user's list using dict.fromkeys(DEFAULTS + user), mirroring ANDROID_DEFAULT_EXTRACT_PACKAGES in the same file.

Two constraints worth recording, both verified:

  • Nested paths do work — copyDirectory passes rootDir unchanged through the recursion, so --exclude src/.venv matches at depth.
  • The separator does not. Dart's path.relative returns src\.venv on Windows, so any multi-segment value Flet emits must be os.sep-joined.

Opt-out

Today user configuration can only extend exclude_list; there is no way to un-exclude. Since this change makes Flet drop more by default, it should ship with --no-default-excludes (and a pyproject.toml equivalent) so anyone we guess wrong about is not forced to pin an older Flet.


Part 2 — what is actually dropped, and why you cannot see it

build is not the whole story, but the rest is not expressible as exclusions either. Four phases, two matchers:

phase mechanism matcher default applies to
copy --exclude exact relative path build (+ assets on web) app dir
compile --compile-app ON app dir — every .py deleted, replaced by .pyc
cleanup app --cleanup-app globs OFF app dir
cleanup packages --cleanup-packages globs ON site-packages

The glob set (serious_python junkFiles) is **.c, **.h, **.cpp, **.hpp, **.typed, **.pyi, **.pxd, **.pyx, **.a, **.pdb, __pycache__, **/__pycache__ — plus **.exe, **.dll, bin, **/bin on mobile. Defaults are at build_base.py:2664-2677.

Should the glob set be folded into the default exclusion list?

Proposed answer: no — but the need behind the question is real.

Against folding:

  • --exclude cannot express globs. It is excludeList.contains(relativePath); **.pyi is unrepresentable.
  • They run in different phases. Exclusions happen before the copy; cleanup after copy and compile. Moving them changes build cost and semantics, not just presentation.
  • They mostly apply to different trees. The one that is ON by default (cleanup-packages) does not touch the app at all — it prunes dependencies.
  • It would couple Flet's defaults to a serious_python internal constant that can change on any version bump.

A nuance worth stating, because it reframes the question: for the app directory, the only thing dropped by default beyond build/assets is .py (turned into .pyc). cleanup-app is off, so a user's own .pyi, .c and bin/ all ship. The hidden pruning is in site-packages.

Proposed instead: report, and document

  • One line at default verbosity naming the default exclusions that actually matched something on disk. "We silently deleted your virtualenv" is exactly the kind of thing that should not be silent — especially as a new default.
  • The full picture at -v, including the cleanup globs and which tree each applied to.
  • Document the four phases. website/docs/publish/index.md:1444 currently says only that build is always excluded, and never mentions the cleanup phase.
  • Possibly a packaging manifest / --dry-run: "show me what would go into the package, and what was dropped and why". That answers the question durably and reflects the user's actual project, which a static docs list never will. Floated for discussion rather than proposed.

Open questions for the team

  1. .env — exclude, or ship and warn? It is the classic secret file, but python-dotenv apps read it at runtime, so silently dropping it turns a size bug into "worked locally, broke when built". Leaning warn.
  2. Is cleanup-app OFF while cleanup-packages is ON deliberate? It is asymmetric and undocumented, and it is why a user's own .pyi/.c ship.
  3. How loud should the report be? One line always, or only under -v?
  4. Should flet publish gain the same defaults, and flet build gain publish's dot-entry rule, so the two packagers stop having complementary blind spots?

Notes

  • Already-built artifacts are unaffected by a fix; anyone who has shipped a bundle containing .venv or .flet may want to check what went out.
  • A changelog entry should call out the behaviour change explicitly, since apps that (unwisely) relied on something dot-prefixed being packaged would change behaviour on upgrade.