← Back to list

From Python Script to Windows EXE: Complete PyInstaller Guide (2025)

Transform your Tkinter and PyQt apps into standalone executables that anyone can run no Python installation required!

Lovnish Verma · 2025-09-09 18:12 · 50 claps · 6.6 min read
#python-tkinter #pyqt5 #python-programming
Open on Medium ↗
Wiki topics: 💻 · Programming

From Python Script to Windows EXE: Complete PyInstaller Guide (2025)

Transform your Tkinter and PyQt apps into standalone executables that anyone can run no Python installation required!

Ever built a cool Python app but struggled to share it with friends who don’t have Python installed? PyInstaller is your answer! It bundles your Python application and all its dependencies into a single executable file that runs on any Windows machine.

What You’ll Learn

  • ✅ Convert Tkinter apps to .exe files
  • ✅ Package PyQt applications as executables
  • ✅ Handle common PyInstaller issues
  • ✅ Optimize file size and startup time
  • ✅ Add icons and version info to your .exe

Estimated time: 30–45 minutes

Prerequisites

  • Windows PC (Windows 10/11 recommended)
  • Python 3.7+ installed with PATH configured
  • Basic Python knowledge (functions, classes)
  • Existing GUI app or willingness to create sample apps

Step 1: Install PyInstaller

Open Command Prompt and install PyInstaller:

pip install pyinstaller

Verify installation:

pyinstaller --version

You should see something like 5.13.2 or newer.

Step 2: Create Sample Applications

Let’s create two sample apps to demonstrate the process.

Tkinter Calculator App

Create tkinter_calculator.py:

import tkinter as tk # Tkinter is included with Python by default; no need to install via pip
from tkinter import messagebox
import math

class Calculator:
    def __init__(self, root):
        self.root = root
        self.root.title("Python Calculator")
        self.root.geometry("300x400")
        self.root.resizable(False, False)

        # Variables
        self.current = "0"
        self.total = 0
        self.input_value = True
        self.result = False

        # Display
        self.display_var = tk.StringVar(value="0")
        display = tk.Entry(
            root, textvariable=self.display_var, 
            font=('Arial', 16), justify='right',
            state='readonly', bd=10
        )
        display.grid(row=0, column=0, columnspan=4, padx=5, pady=5, sticky="ew")

        # Buttons
        self.create_buttons()

    def create_buttons(self):
        buttons = [
            ('C', 1, 0), ('±', 1, 1), ('%', 1, 2), ('/', 1, 3),
            ('7', 2, 0), ('8', 2, 1), ('9', 2, 2), ('*', 2, 3),
            ('4', 3, 0), ('5', 3, 1), ('6', 3, 2), ('-', 3, 3),
            ('1', 4, 0), ('2', 4, 1), ('3', 4, 2), ('+', 4, 3),
            ('0', 5, 0), ('.', 5, 2), ('=', 5, 3)
        ]

        for (text, row, col) in buttons:
            if text == '0':
                btn = tk.Button(
                    self.root, text=text, font=('Arial', 14),
                    command=lambda t=text: self.button_click(t)
                )
                btn.grid(row=row, column=col, columnspan=2, padx=2, pady=2, sticky="ew")
            else:
                btn = tk.Button(
                    self.root, text=text, font=('Arial', 14),
                    command=lambda t=text: self.button_click(t)
                )
                btn.grid(row=row, column=col, padx=2, pady=2, sticky="ew")

    def button_click(self, value):
        if value in '0123456789':
            if self.input_value:
                self.current = value
                self.input_value = False
            else:
                self.current += value
            self.display_var.set(self.current)

        elif value == 'C':
            self.current = "0"
            self.total = 0
            self.input_value = True
            self.display_var.set("0")

        elif value == '=':
            try:
                self.current = str(eval(self.current))
                self.display_var.set(self.current)
                self.input_value = True
            except:
                messagebox.showerror("Error", "Invalid calculation")

        elif value in '+-*/':
            if not self.input_value:
                self.current += value
                self.input_value = False
            self.display_var.set(self.current)

if __name__ == "__main__":
    root = tk.Tk()
    app = Calculator(root)
    root.mainloop()

PyQt Text Editor App

Create pyqt_editor.py:

import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, 
                             QMenuBar, QAction, QFileDialog, QMessageBox,
                             QVBoxLayout, QWidget)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont

class TextEditor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Python Text Editor")
        self.setGeometry(100, 100, 800, 600)

        # Central widget
        self.text_edit = QTextEdit()
        self.text_edit.setFont(QFont("Courier", 12))
        self.setCentralWidget(self.text_edit)

        # Current file
        self.current_file = None

        # Create menu
        self.create_menu()

    def create_menu(self):
        menubar = self.menuBar()

        # File menu
        file_menu = menubar.addMenu('File')

        new_action = QAction('New', self)
        new_action.setShortcut('Ctrl+N')
        new_action.triggered.connect(self.new_file)
        file_menu.addAction(new_action)

        open_action = QAction('Open', self)
        open_action.setShortcut('Ctrl+O')
        open_action.triggered.connect(self.open_file)
        file_menu.addAction(open_action)

        save_action = QAction('Save', self)
        save_action.setShortcut('Ctrl+S')
        save_action.triggered.connect(self.save_file)
        file_menu.addAction(save_action)

        file_menu.addSeparator()

        exit_action = QAction('Exit', self)
        exit_action.setShortcut('Ctrl+Q')
        exit_action.triggered.connect(self.close)
        file_menu.addAction(exit_action)

    def new_file(self):
        self.text_edit.clear()
        self.current_file = None
        self.setWindowTitle("Python Text Editor - New File")

    def open_file(self):
        filename, _ = QFileDialog.getOpenFileName(
            self, "Open File", "", 
            "Text files (*.txt);;Python files (*.py);;All files (*.*)"
        )
        if filename:
            try:
                with open(filename, 'r', encoding='utf-8') as file:
                    content = file.read()
                    self.text_edit.setPlainText(content)
                    self.current_file = filename
                    self.setWindowTitle(f"Python Text Editor - {filename}")
            except Exception as e:
                QMessageBox.warning(self, "Error", f"Could not open file:\n{str(e)}")

    def save_file(self):
        if self.current_file:
            self.save_to_file(self.current_file)
        else:
            self.save_as_file()

    def save_as_file(self):
        filename, _ = QFileDialog.getSaveFileName(
            self, "Save File", "", 
            "Text files (*.txt);;Python files (*.py);;All files (*.*)"
        )
        if filename:
            self.save_to_file(filename)

    def save_to_file(self, filename):
        try:
            with open(filename, 'w', encoding='utf-8') as file:
                file.write(self.text_edit.toPlainText())
                self.current_file = filename
                self.setWindowTitle(f"Python Text Editor - {filename}")
                QMessageBox.information(self, "Success", "File saved successfully!")
        except Exception as e:
            QMessageBox.warning(self, "Error", f"Could not save file:\n{str(e)}")

if __name__ == '__main__':
    app = QApplication(sys.argv)
    editor = TextEditor()
    editor.show()
    sys.exit(app.exec_())

Install PyQt5 if you haven’t already:

pip install PyQt5

Step 3: Basic EXE Creation

For Tkinter App

Navigate to your project folder and run:

pyinstaller --onefile --windowed tkinter_calculator.py

For PyQt App

pyinstaller --onefile --windowed pyqt_editor.py

Command breakdown:

  • --onefile: Creates single .exe file (vs. folder with multiple files)
  • --windowed: Hides console window (important for GUI apps)
  • filename.py: Your Python script

Build process:

  1. PyInstaller analyzes your script
  2. Identifies all dependencies
  3. Creates build files in dist/ folder
  4. Your .exe appears in dist/filename.exe

Step 4: Understanding PyInstaller Options

Essential Options

# Most common combination for GUI apps
pyinstaller --onefile --windowed --name "My Calculator" tkinter_calculator.py

# Add custom icon
pyinstaller --onefile --windowed --icon=calculator.ico tkinter_calculator.py

# Specify additional files/folders
pyinstaller --onefile --windowed --add-data "config.txt;." app.py

# Hide imports (cleaner build)
pyinstaller --onefile --windowed --hidden-import=tkinter app.py

File Size Optimization

# Exclude unnecessary modules
pyinstaller --onefile --windowed --exclude-module matplotlib tkinter_calculator.py

# Use UPX compression (install UPX separately)
pyinstaller --onefile --windowed --upx-dir /path/to/upx tkinter_calculator.py

Step 5: Adding Icons and Metadata

Create an Icon File

  1. Find an icon (PNG, JPG) or create one
  2. Convert to .ico format using online converters like:
  1. Save as app_icon.ico in your project folder

Build with Icon

pyinstaller --onefile --windowed --icon=app_icon.ico tkinter_calculator.py

Add Version Information (Advanced)

Create version_info.txt:

VSVersionInfo(
  ffi=FixedFileInfo(
    filevers=(1, 0, 0, 0),
    prodvers=(1, 0, 0, 0),
    mask=0x3f,
    flags=0x0,
    OS=0x40004,
    fileType=0x1,
    subtype=0x0,
    date=(0, 0)
  ),
  kids=[
    StringFileInfo(
      [
        StringTable(
          u'040904B0',
          [StringStruct(u'CompanyName', u'Your Company'),
           StringStruct(u'FileDescription', u'Python Calculator'),
           StringStruct(u'FileVersion', u'1.0.0.0'),
           StringStruct(u'ProductName', u'Calculator App'),
           StringStruct(u'ProductVersion', u'1.0.0.0')])
      ]),
    VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
  ]
)

Build with version info:

pyinstaller --onefile --windowed --version-file=version_info.txt tkinter_calculator.py

Step 6: Handling Common Issues

Issue 1: “Module not found” Errors

Problem: PyInstaller can’t find all dependencies Solution: Add hidden imports

pyinstaller --onefile --windowed --hidden-import=tkinter.messagebox tkinter_calculator.py

Issue 2: Large File Sizes

Problem: .exe file is 50MB+ for simple app Solutions:

  1. Use virtual environment (recommended):
# Create clean environment
python -m venv pyinstaller_env
pyinstaller_env\Scripts\activate
pip install pyinstaller tkinter

# Build in clean environment
pyinstaller --onefile --windowed app.py
  1. Exclude unused modules:
pyinstaller --onefile --windowed --exclude-module numpy --exclude-module pandas app.py

Issue 3: Slow Startup Times

Problem: .exe takes 10+ seconds to start Solutions:

  1. Use — onedir instead of — onefile:
pyinstaller --windowed --onedir tkinter_calculator.py
  1. Optimize imports in your Python code:
# Instead of importing entire modules
import tkinter as tk
from tkinter import messagebox

# Import only what you need

Issue 4: Antivirus False Positives

Problem: Antivirus software flags your .exe Solutions:

  1. Add exclusions in your antivirus software
  2. Use code signing certificates (for distribution)
  3. Submit to antivirus vendors for whitelisting

Step 7: Advanced PyInstaller Features

Custom Build Specs

Generate a .spec file for advanced customization:

pyinstaller --name="Calculator" --onefile --windowed --specpath=. tkinter_calculator.py

Edit the generated Calculator.spec file:

# -*- mode: python ; coding: utf-8 -*-

a = Analysis(
    ['tkinter_calculator.py'],
    pathex=[],
    binaries=[],
    datas=[('config/', 'config/')],  # Include data files
    hiddenimports=['tkinter.messagebox'],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=['matplotlib', 'numpy'],  # Exclude unused modules
    noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='Calculator',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,  # Enable UPX compression
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,  # Hide console
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon='calculator.ico'  # Custom icon
)

Build from spec file:

pyinstaller Calculator.spec

Step 8: Testing and Distribution

Test Your EXE

  1. Test on your machine first
  2. Copy to different folder and run (ensures all dependencies included)
  3. Test on clean Windows VM if possible
  4. Check different Windows versions (7, 10, 11)

Distribution Options

Option 1: Direct Distribution

  • Share the .exe file directly
  • Include any additional files your app needs
  • Consider creating an installer with tools like Inno Setup

Option 2: Create Installer

  • Use Inno Setup (free) or NSIS
  • Package your .exe with installer
  • Add uninstall functionality

Option 3: Code Signing

  • Purchase code signing certificate
  • Sign your .exe to avoid security warnings
  • Important for professional distribution

Step 9: Optimization Tips

Reduce File Size

# 1. Use virtual environment
python -m venv build_env
build_env\Scripts\activate
pip install only-required-packages

# 2. Exclude unused modules
pyinstaller --exclude-module PIL --exclude-module requests app.py

# 3. Use UPX compression
pyinstaller --upx-dir C:\upx app.py

Improve Performance

# 1. Lazy imports in your code
def advanced_function():
    import numpy as np  # Import only when needed
    # Function code here

# 2. Minimize startup code
if __name__ == "__main__":
    # Keep this section minimal
    app = MyApp()
    app.run()

Debug Issues

# Run with console to see errors
pyinstaller --onefile --console app.py

# Verbose output during build
pyinstaller --log-level DEBUG app.py

Quick Reference Commands

Most Common Builds

# Simple GUI app
pyinstaller --onefile --windowed app.py

# GUI app with icon
pyinstaller --onefile --windowed --icon=icon.ico app.py

# App with additional files
pyinstaller --onefile --windowed --add-data "data.txt;." app.py

# Optimized build (smaller size)
pyinstaller --onedir --windowed --exclude-module numpy app.py

Troubleshooting Commands

# Show console for debugging
pyinstaller --onefile --console app.py

# Clean build (remove old files)
pyinstaller --clean --onefile --windowed app.py

# Verbose logging
pyinstaller --log-level DEBUG app.py

Common Tkinter vs PyQt Considerations

Tkinter Specifics

  • Usually smaller .exe files
  • Fewer dependencies to bundle
  • Faster build times
  • May need --hidden-import tkinter.messagebox

PyQt Specifics

  • Larger .exe files (30MB+)
  • More dependencies (Qt libraries)
  • Longer build times
  • May need additional Qt plugins

Best Practices Summary

  1. Always test your Python script thoroughly before building
  2. Use virtual environments for cleaner builds
  3. Start with — onedir for faster development, switch to — onefile for distribution
  4. Add icons and metadata for professional appearance
  5. Test on clean systems without Python installed
  6. Consider file size vs. convenience trade-offs
  7. Document dependencies and build process for team members

Conclusion

You now have everything you need to convert your Python GUI applications into standalone executables! Whether you’re building with Tkinter or PyQt, PyInstaller makes it straightforward to create professional-looking applications that anyone can run.

Key Takeaways:

  • PyInstaller handles dependency bundling automatically
  • Use --onefile --windowed for most GUI applications
  • Test thoroughly on systems without Python
  • Consider file size vs. convenience trade-offs
  • Add icons and metadata for professional polish

Start with simple applications and gradually work your way up to more complex projects. The more you practice with PyInstaller, the more comfortable you’ll become with its various options and optimizations.

Happy building! 🐍💻✨

Need help with a specific PyInstaller issue? Drop a comment below and I’ll help you troubleshoot!


메타데이터
post_id
4b22cd7461c5
slug
from-python-script-to-windows-exe-complete-pyinstaller-guide-2025-4b22cd7461c5
url
https://medium.com/@lovnish/from-python-script-to-windows-exe-complete-pyinstaller-guide-2025-4b22cd7461c5
canonical_url
https://medium.com/@lovnish/from-python-script-to-windows-exe-complete-pyinstaller-guide-2025-4b22cd7461c5
author_url
https://medium.com/@lovnish
status
ok
fetched_at
2026-07-17 15:44:51