Skip to main content
Editor

tkinter to .exe with PyInstaller: Windows, macOS and Linux

Intermediate20 min2 exercises25 XP
0/2 exercises

Turn your tkinter program into an .exe, a Mac app or a Linux program that people can open with a double-click, with no Python on their computer.

Write and test the app at tkinter online or in the editor, where the window opens in the Window tab. Save the file with Download .py under the window on the tkinter page, or in the editor by right-clicking the file's tab and choosing Download .py. The program is standard tkinter, so the same file runs with Python on your computer.

PyInstaller does not compile your code to machine code. It copies the Python interpreter, your program as compiled .pyc files, every module the program imports and, for tkinter, the Tcl/Tk files, then adds a small launcher. The result runs only on the kind of system that built it: build the .exe on Windows, the Mac app on a Mac and the Linux program on Linux.

The example app

The steps below package this tip calculator. Any tkinter program works the same way: copy this one into the tkinter page to try it first.

app.py
import tkinter as tk
from tkinter import ttk


def calculate():
    try:
        bill = float(bill_entry.get())
        percent = float(tip_box.get())
    except ValueError:
        result.set("Type numbers, like 48.50 and 15")
        return
    tip = bill * percent / 100
    result.set(f"Tip: {tip:.2f}   Total: {bill + tip:.2f}")


root = tk.Tk()
root.title("Tip Calculator")

frame = ttk.Frame(root, padding=16)
frame.grid()

ttk.Label(frame, text="Bill").grid(row=0, column=0, sticky="w")
bill_entry = ttk.Entry(frame, width=12)
bill_entry.grid(row=0, column=1, pady=4)

ttk.Label(frame, text="Tip %").grid(row=1, column=0, sticky="w")
tip_box = ttk.Spinbox(frame, from_=0, to=30, width=10)
tip_box.set(15)
tip_box.grid(row=1, column=1, pady=4)

ttk.Button(frame, text="Calculate", command=calculate).grid(row=2, column=0, columnspan=2, pady=8)

result = tk.StringVar()
ttk.Label(frame, textvariable=result).grid(row=3, column=0, columnspan=2)

root.mainloop()

Build an .exe on Windows

1. Check Python and tkinter. Put app.py in a folder of its own, open a terminal (Command Prompt or PowerShell) and go to that folder with cd. The last command opens a small test window when tkinter works; close it to go on.

Command Prompt or PowerShell
cd path\to\tip-calculator
python --version
python -m tkinter

If python is not found, install Python from python.org and open a new terminal.

2. Run the program with Python. PyInstaller packages the program as it is, so fix any error here first.

Run it
python app.py

3. Install PyInstaller.

Install
python -m pip install pyinstaller

4. Build the .exe.

Build
pyinstaller --onefile --windowed app.py

If the terminal does not know pyinstaller, run python -m PyInstaller --onefile --windowed app.py instead (with a capital P and I).

5. Try it. Your program is dist\app.exe. Double-click it: the tip calculator opens with no console window behind it. To share it, send that one file.

PyInstaller also leaves a build folder of working files and app.spec, which records your options. Neither is needed to run the program.

OptionWhat it does
--onefilePacks everything into one .exe (without it you get a folder)
--windowedNo console window behind the app; -w and --noconsole mean the same
--name "Tip Calculator"Names the program (the default is the script's name)
--icon app.icoGives the .exe file an icon
--add-data "logo.png:."Puts a file your app opens into the bundle
--hidden-import nameAdds a module PyInstaller did not find in your code

One file or one folder

Without --onefile, PyInstaller makes a folder, dist\app, holding app.exe and an _internal folder with Python and everything else. The program runs from that folder, so you share the whole folder, zipped.

One file (--onefile)One folder (the default)
What you shareOne .exeThe folder, zipped
Starting upA little slower: it unpacks itself into a temporary _MEI folder every timeFaster: nothing to unpack
Finding build problemsHarderEasier: PyInstaller's manual suggests getting a one-folder build working first

The temporary _MEI folder is deleted when the program closes, but stays behind if it crashes or is killed. For a small tkinter app on Windows, one file is the easiest to hand around.

Name and icon

--name names the program and --icon gives the .exe an icon from an .ico file. With Pillow installed (python -m pip install pillow), PyInstaller also accepts a PNG and converts it.

Build with a name and an icon
pyinstaller --onefile --windowed --name "Tip Calculator" --icon app.ico app.py

The program is now dist\Tip Calculator.exe.


Ship images and other files

A program that opens a file by a bare name, like tk.PhotoImage(file="logo.png"), looks for it in the working folder: the folder the program was started from. That works when you run python app.py in the project folder. In the .exe it fails twice over: logo.png is not in the bundle, and the bundle's files are not in the working folder anyway.

Put the file in the bundle with --add-data "SOURCE:DEST": the file on your computer, a colon, then the folder inside the bundle, where . is the top folder. Repeat the option for each file, or give a whole folder.

Add a logo and a folder of sounds
pyinstaller --onefile --windowed --add-data "logo.png:." --add-data "sounds:sounds" app.py

Open the file by a full path built from the folder your script is in:

Find files next to the script, in a bundle or not
import os
import tkinter as tk

HERE = os.path.dirname(os.path.abspath(__file__))

root = tk.Tk()
logo = tk.PhotoImage(file=os.path.join(HERE, "logo.png"))
tk.Label(root, image=logo).pack()
root.iconphoto(True, logo)
root.mainloop()

Run as a script, __file__ is the path of app.py. Inside a bundle, PyInstaller sets __file__ to a path inside the bundle folder, which is where --add-data put logo.png. PyInstaller's manual calls __file__ the preferred way to find bundled files.

A bundled program can also tell where it is: PyInstaller sets sys.frozen to True and sys._MEIPASS to the bundle folder, which is the temporary _MEI folder of a one-file program or the _internal folder of a one-folder one. Many answers online use a resource_path() helper built on sys._MEIPASS; you write one in the first exercise below.

Am I running from a bundle?
import sys
def bundle_folder():
"""The folder PyInstaller unpacked the app into, or None for a plain script."""
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
return sys._MEIPASS
return None
print("Run as a script:", bundle_folder())
# Pretend to be a one-file .exe that Windows has unpacked
sys.frozen = True
sys._MEIPASS = r"C:\Users\ana\AppData\Local\Temp\_MEI52162"
print("Run as an .exe: ", bundle_folder())
del sys.frozen, sys._MEIPASS
Output
Run as a script: None
Run as an .exe:  C:\Users\ana\AppData\Local\Temp\_MEI52162

Rebuild with the spec file

Running pyinstaller on app.py writes app.spec afresh every time (Tip Calculator.spec with --name). The spec is Python code holding your options; to keep changes you make in it, build from the spec instead:

Build from the spec
pyinstaller app.spec

Files to bundle go in the datas= line inside Analysis(...), as (source, folder in the bundle) pairs:

app.spec (the datas line)
    datas=[('logo.png', '.'), ('sounds', 'sounds')],

A build from a spec ignores most command-line options, since the spec holds them. Two that still work: --noconfirm replaces the last build without asking, and --clean clears PyInstaller's cache and temporary files before building.


Build a Mac app

On a Mac the steps are the same, with python3 in place of python. The python.org installer includes Tk. With Homebrew's Python, also install its Tk package for your Python version, for example brew install python-tk@3.14.

Terminal (macOS)
cd path/to/tip-calculator
python3 -m tkinter
python3 -m pip install pyinstaller
pyinstaller --windowed --name "Tip Calculator" app.py

If pip stops with externally-managed-environment, install PyInstaller in a virtual environment, as in the Linux steps below.

On macOS --windowed makes an app: dist/Tip Calculator.app, which opens with a double-click. Leave out --onefile: an app is a folder anyway, and PyInstaller warns that one-file Mac apps clash with macOS security and will be refused from PyInstaller 7. For the icon, --icon takes an .icns file, or a PNG when Pillow is installed.

Zip the app to share it. macOS warns before opening an app from the internet that Apple has not checked. The person opening it tries once, then clicks Open Anyway in System Settings > Privacy & Security. To remove the warning for everyone, sign the app with a Developer ID certificate from the Apple Developer Program and send it to Apple for notarization.

Build a Linux program

Install tkinter, the venv module and binutils (PyInstaller uses its objdump and objcopy). On Debian or Ubuntu:

Debian or Ubuntu
sudo apt install python3-tk python3-venv binutils

On Fedora the tkinter package is python3-tkinter. Recent Debian and Ubuntu refuse pip install outside a virtual environment, so build inside one:

Terminal (Linux)
cd path/to/tip-calculator
python3 -m venv .venv
source .venv/bin/activate
pip install pyinstaller
pyinstaller --onefile --name tip-calculator app.py
./dist/tip-calculator

The program is dist/tip-calculator, with no file extension. --windowed does nothing on Linux. A program built on a new Linux release may not start on an older one, so build on the oldest release you want it to run on.


When the app will not start

Failed to execute script

When a windowed .exe crashes, Windows shows a box titled Unhandled exception in script that says Failed to execute script 'app' due to unhandled exception, with the traceback under it. To see the error the way Python prints it, build without --windowed and start the program from a terminal, so the window stays open after the crash:

Build with a console and run it
pyinstaller --onefile app.py
.\dist\app.exe

On Linux run ./dist/app the same way. On a Mac, run the program inside the app: "dist/Tip Calculator.app/Contents/MacOS/Tip Calculator". The last line of the traceback names the problem:

The error saysWhat to do
couldn't open "logo.png" or FileNotFoundErrorThe file is missing from the bundle or opened by a bare name: add it with --add-data and open it by a full path (see above)
ModuleNotFoundErrorPyInstaller missed an import it could not see in your code, such as one made by name with importlib: add --hidden-import and the module name
RuntimeError: lost sys.stdinThe app calls input(), which a windowed build cannot answer (see below)

PyInstaller lists the modules it could not find in build\app\warn-app.txt. Most entries there are harmless; look for the name from your error.

Antivirus or SmartScreen warnings

Some antivirus programs flag PyInstaller programs, harmless ones included, because malware is built with PyInstaller too and the scanners judge by resemblance; windowed builds are flagged more often. PyInstaller's maintainers suggest three things: report the false positive to the antivirus maker (Microsoft Defender takes files on its submission page), sign the program with a code-signing certificate, which makes warnings less common without ending them, or build PyInstaller's launcher (the bootloader) yourself.

Windows SmartScreen also warns about new programs downloaded from the internet that few people have run yet. More info, then Run anyway, opens the program.

Other tools

auto-py-to-exe puts PyInstaller's options in a window: install it with python -m pip install auto-py-to-exe, then run auto-py-to-exe. Nuitka works differently: it translates your program to C and compiles that, so it needs a C compiler, and it can also make one-file programs.


Practice exercises

Code on this page runs in your browser, with no bundle and no script file, so the tests play both parts: they set sys._MEIPASS to pretend to be a bundle, and set __file__ to a pretend script path.

Find Bundled Files
Write Code

Write resource_path(relative) that returns the full path of a file your app ships with:

  • In a PyInstaller bundle, sys has a _MEIPASS attribute: join relative onto that folder.
  • Otherwise, join it onto the folder of the script, os.path.dirname(os.path.abspath(__file__)).
  • # in a bundle unpacked to /tmp/_MEI52162
    resource_path("logo.png")  ->  /tmp/_MEI52162/logo.png
    
    # as the script /home/ana/tip-calculator/app.py
    resource_path("logo.png")  ->  /home/ana/tip-calculator/logo.png
    import os
    import sys
    def resource_path(relative):
    # In a bundle: start from sys._MEIPASS
    # Otherwise: start from the folder of __file__
    pass
    Build the --add-data Options
    Write Code

    Your app opens resource_path("logo.png") and resource_path("sounds/ding.wav"). For each file to land where that code looks, it needs its own --add-data SOURCE:DEST, where DEST is the folder inside the bundle: logo.png:. and sounds/ding.wav:sounds.

    Write add_data_args(paths) that takes the relative paths your code opens and returns the list of arguments to add to the pyinstaller command: "--add-data", then "SOURCE:DEST", for each path in order. DEST is the folder part of the path, or . when there is no folder.

    add_data_args(["logo.png", "sounds/ding.wav"])
    ->  ['--add-data', 'logo.png:.', '--add-data', 'sounds/ding.wav:sounds']
    import os
    def add_data_args(paths):
    # One "--add-data" and one "SOURCE:DEST" per path
    pass

    Related Tutorials

    Was this tutorial helpful?