Skip to main content
Guides

Build a Python .exe with uv and PyInstaller (Tested)

By Mithun··7 min read
Quick Answer

To turn a Python project into a standalone .exe with uv: run uv init, add dependencies with uv add, add PyInstaller with uv add --dev pyinstaller, then build with uv run pyinstaller --onefile app.py. uv manages the Python version, the dependencies, and PyInstaller in one reproducible environment, so the build bundles third-party packages correctly. For a quick standard-library-only Windows build with nothing to install, use our online Python-to-EXE builder instead.

uv has become the fastest way to manage Python projects — it resolves and installs dependencies in milliseconds, can download the Python interpreter itself, and pins everything in a lockfile. PyInstaller turns a Python program into a standalone executable. Put them together and you get a clean, reproducible path from source to a shippable .exe — one that correctly bundles third-party packages, which is where hand-rolled builds usually go wrong.

Everything below was actually run: uv 0.12.1 and PyInstaller 6.22.2 on Windows, building a real app that depends on the rich library. If you only need a quick Windows .exe from a standard-library script and don't want to install anything, our online Python-to-EXE builder does that in the browser — this guide is the local route for apps with real dependencies or non-Windows targets.

Why build with uv?

PyInstaller bundles whatever's in the current environment. The classic failure is an .exe that crashes with ModuleNotFoundError because a dependency wasn't installed where PyInstaller looked. uv removes that whole class of problem: it owns the project's Python and its dependencies, so uv run pyinstaller builds against *exactly* the packages your app declares — and a uv.lock makes it reproducible for anyone who checks out the repo.

  • One environment — Python, your deps, and PyInstaller all managed by uv; no manual venv activation.
  • Reproducibleuv.lock pins exact versions, so the build isn't a 'works on my machine' artifact.
  • Fast — dependency resolution and installs are near-instant.
  • Third-party packages bundle correctly — the reason to leave the browser: our online builder is standard-library-only, this handles rich, requests, numpy, anything.

Set up the project and add dependencies

Create the project — uv will fetch the Python version you name if you don't have it — then add a real dependency. Here we use rich so the build has something non-stdlib to bundle:

# 1. create the project (uv picks/downloads the Python for you)
uv init greeter --python 3.12
cd greeter
# 2. add a real runtime dependency
uv add rich
Tested with uv 0.12.1

uv add rich resolved and installed rich 15.0.0 (plus its own deps markdown-it-py, mdurl, pygments) in a few hundred milliseconds, wrote rich>=15.0.0 into pyproject.toml, and pinned exact versions in uv.lock. Now a small program that uses it:

import sys
from rich.console import Console
from rich.panel import Panel
def main():
console = Console()
name = sys.argv[1] if len(sys.argv) > 1 else "world"
console.print(Panel(f"Hello, {name}!", title="greeter", border_style="green"))
if __name__ == "__main__":
main()
app.py — uses the third-party rich library

Add PyInstaller and build the .exe

Add PyInstaller as a dev-only dependency, then build. Because uv run executes inside the project's environment, PyInstaller sees rich and bundles it automatically:

# PyInstaller as a dev-only dependency
uv add --dev pyinstaller
# build: uv runs PyInstaller inside the project env, so it sees rich
uv run pyinstaller --onefile --name greeter app.py
Add the packager and build — tested

The one-file binary lands in dist/. In my run (uv 0.12.1, PyInstaller 6.22.2) this produced dist/greeter.exe at 11.3 MB — bigger than a standard-library-only build because rich, pygments and markdown-it-py are bundled in. Prefer not to add PyInstaller to the project at all? uvx runs it one-shot:

uvx pyinstaller --onefile --name greeter app.py
uvx: run PyInstaller without adding it as a dependency

uvx (an alias for uv tool run) fetches PyInstaller into a temporary, isolated tool environment and runs it — handy for a one-off build, though for repeatable CI builds adding it with uv add --dev keeps the version locked.

Run it — the dependency really is bundled

The whole point is a single file that runs on a machine with no Python and no pip install. Running the built binary shows rich working from inside the bundle:

> .\dist\greeter.exe Acme
+---------------------------------- greeter ----------------------------------+
| Hello, Acme! |
+-----------------------------------------------------------------------------+
Real output from the built greeter.exe on Windows

That box is drawn by rich — a third-party package — running from inside the standalone .exe. No interpreter, no dependencies installed on the target machine.

Make the build reproducible

uv add writes a uv.lock (162 lines for this tiny project) that pins every transitive dependency to an exact version and hash. Commit it, and anyone can reproduce your build exactly:

uv sync # install the exact locked versions
uv run pyinstaller --onefile --name greeter app.py
Reproduce the environment from the lockfile, then build

This is the difference between a build that happens to work today and one that still builds identically in six months or on a CI runner — the dependency versions can't drift underneath you.

Cross-platform, and when to use the online builder instead

PyInstaller cannot cross-compile: run it on the OS you're targeting — Windows for a .exe, macOS for an app bundle, Linux for an ELF binary. The uv workflow above works on all three; just run it on each machine (or each CI runner).

So which route should you use?

Online builderuv + PyInstaller (this guide)
Third-party packagesNo (stdlib only)Yes — anything you uv add
Target OSWindows x64Any (build on that OS)
Install neededNone — runs in the browserInstall uv (one command)
Best forQuick stdlib script → .exeReal apps with dependencies

For a quick standard-library script, our online Python-to-EXE builder returns a Windows .exe with nothing to install. Reach for uv + PyInstaller when your app has dependencies or you're targeting macOS/Linux. Either way, remember a packaged .exe does not protect your source — the bytecode can be extracted, so obfuscate it first if it's proprietary.

Common gotchas

  • `ModuleNotFoundError` at runtime — PyInstaller missed a dynamically imported module. Add it with --hidden-import <module>. Building with uv already prevents the most common cause (the dependency not being installed at all).
  • Missing data files — bundle them with --add-data "src;dest" (PyInstaller 6+ accepts a : separator on every platform).
  • Big executables — a onefile build bundles the interpreter and every dependency, so pulling in rich took the binary to 11.3 MB. Use --onedir for faster startup, or trim unused dependencies.
  • Windows SmartScreen warning on the finished .exe — expected for any new, unsigned download; see the explanation and fixes on our Python-to-EXE guide.

Just need a quick .exe? Skip the setup

For a standard-library Python script, our online builder returns a standalone Windows .exe in your browser — no uv, no PyInstaller, no install. Bring uv + PyInstaller back when you have dependencies.

Open the Python-to-EXE builder

Free tools mentioned here

Related guides

Frequently asked questions

How do I build a Python .exe with uv?

Run uv init to create the project, uv add for each dependency, uv add --dev pyinstaller to add the packager, then uv run pyinstaller --onefile app.py. Because uv runs PyInstaller inside the project's environment, the build sees your dependencies and bundles them; the executable is written to the dist/ folder.

Can I run PyInstaller without adding it to my project?

Yes. Use uvx pyinstaller --onefile app.py (uvx is short for uv tool run). It fetches PyInstaller into an isolated, temporary tool environment and runs it, so nothing is added to your project's dependencies. For repeatable CI builds, prefer uv add --dev pyinstaller so the version is locked.

Does uv + PyInstaller bundle third-party packages?

Yes — that's the main advantage over a browser-based builder. uv installs your declared dependencies into the project environment, and uv run pyinstaller builds against exactly those, so packages like rich, requests or numpy are bundled into the executable. In my test the built greeter.exe rendered rich output with no Python installed on the machine.

Why is my uv PyInstaller .exe so large?

A onefile build embeds the Python interpreter plus every dependency, so size grows with your imports. Adding rich took the example binary to about 11.3 MB. Use --onedir for a folder build that starts faster, remove unused dependencies, or keep the app standard-library-only where possible.

Can uv build a Windows .exe on macOS or Linux?

No. PyInstaller cannot cross-compile, so a Windows .exe must be built on Windows, a macOS app on macOS, and a Linux binary on Linux. The uv workflow is identical on each OS — run it on the target platform or a matching CI runner. For a quick Windows .exe with no install, use our online Python-to-EXE builder.

Does packaging with PyInstaller protect my source code?

No. PyInstaller bundles your compiled bytecode, which can be extracted from the executable and decompiled — packaging is for distribution, not protection. If the code is proprietary, obfuscate the source before you build the executable.

Keep reading