Crafting a Standalone Executable with PyInstaller

Moraneus
9 min readMar 9, 2024

Creating an executable from a Python script can significantly ease the distribution and execution process, making your application more accessible to users without Python installed. PyInstaller is a popular tool that simplifies this process by packaging Python scripts into standalone executables for Windows, Linux, and macOS. Here’s a comprehensive guide to using PyInstaller to create an executable from a Python script.

Introduction to PyInstaller

You’ve developed a great Python script, and now you want to share it with the world. However, not everyone has Python installed, and figuring out how to run your script can be challenging for them. That’s where PyInstaller comes in handy. It’s a tool that takes your script, bundles all its dependencies, and creates a single executable file. This executable can be run with a simple double-click, without requiring Python to be installed. It’s an excellent way to make your Python creations accessible to a broader audience.

Setting Up PyInstaller

Before using PyInstaller, ensure that Python is installed on your computer (version 3.5 or later). Once Python is set up, installing PyInstaller is straightforward. Open your command line tool (Terminal on macOS and Linux, Command Prompt on Windows) and run the following command:

pip install pyinstaller

From Script to Executable: Key Steps

By following these simplified steps, you can easily create an executable version of your Python script using PyInstaller.

1. Prepare Your Script: Before anything else, make sure your script runs smoothly in your Python environment. This means checking for and fixing any bugs or issues. It’s crucial to have your script in tip-top shape before you transform it into an executable, to ensure it behaves as expected when others use it.

2. Navigate to Your Script Directory: Open your command line or terminal. You’ll need to move into the directory where your Python script is saved. If you’re not familiar with command line navigation, you can use the cd command followed by the path to your script's folder to get there.

3. Run PyInstaller: Now, it’s time to invoke PyInstaller to do its magic. Type the command below, making sure to replace your_script.py with the actual name of your Python script:

pyinstaller --onefile your_script.py

The --onefile option is what tells PyInstaller to pack everything your script needs into one neat executable file. Without this option, PyInstaller would instead create a folder filled with your executable and any necessary support files, which can be a bit messier to distribute.

4. Locate Your Executable: Once PyInstaller has done its job, it’s time to grab your newly created executable. You’ll find it waiting in the dist folder, which is located inside the same directory as your script. This file is now a standalone version of your Python script, ready to be shared and run on any compatible system, no Python installation required.

Script-to-Executable Examples with PyInstaller

Let’s improve the guide by providing Python script examples corresponding to each shell command used for creating an executable with PyInstaller. This approach will help clarify how each command applies to specific script scenarios, ranging from simple scripts to those with external data, custom icons, and even GUI applications.

Simple Script Example

Python Script (hello_pyinstaller.py):

# hello_pyinstaller.py

print("Hello, Pyinstaller!")

Shell Command:

pyinstaller --onefile hello_pyinstaller.py

Including Data Files

Perhaps your script, app_with_data.py, relies on external files for its wisdom. PyInstaller allows you to include these essential companions.

When PyInstaller compiles your Python script into an executable, the file structure within the executable differs from your original development environment. Consequently, direct file paths (like ‘data/data_file.txt’ in your script) often fail to resolve correctly.

To address this, you need to modify your script to dynamically locate the data file at runtime, regardless of whether it’s being run as a script or as a compiled executable. PyInstaller sets a _MEIPASS attribute in the sys module for this purpose. This attribute contains the path to the temporary folder that PyInstaller uses to extract your bundled files when the executable runs.

Python Script (app_with_data.py):

import os
import sys

def load_data(file_path):
try:
with open(file_path, 'r') as file:
data = file.read()
return data
except FileNotFoundError:
return "File not found."

def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, relative_path)

if __name__ == "__main__":
# Adjust the path to where you actually store 'data_file.txt'
data_path = resource_path('data/data_file.txt')
print(load_data(data_path))

This resource_path function checks if your script is running in a bundled context by looking for _MEIPASS. If it exists, it means your script is running inside a PyInstaller bundle, and it adjusts the path accordingly. Otherwise, it assumes the script is running in a development environment and uses the standard path.

This setup ensures that your executable can correctly locate and access data_file.txt, preventing the FileNotFoundError you've encountered.

Shell Command (Windows):

pyinstaller --onefile --add-data 'data\data_file.txt;data' app_with_data.py

In this command, 'data\data_file.txt;data' tells PyInstaller to take data_file.txt from the data directory and place it in the data directory of the bundled application.

Shell Command (Linux/Mac):

pyinstaller --onefile --add-data 'data/data_file.txt:data' app_with_data.py

The same happens here. This ensures your script and its data files remain inseparable, no matter where they go.

Notice the use of a colon (:) as the separator in the Linux command instead of a semicolon (;), which is used in Windows.

Including Binary Files

Similar to adding data files, you can include binary dependencies not automatically detected by PyInstaller using the --add-binary option. This is particularly useful for including shared libraries and other binary resources. Do not forget to use the resource_path function as mentioned in the --add-data section.

Windows Example

Let’s say your application uses a specific dynamic link library (DLL) that PyInstaller does not automatically include. You can manually add this file using the --add-binary option. For example, if you have a library called example.dll located in a folder named libs next to your script, you would include it like this:

pyinstaller --onefile --add-binary 'libs\example.dll;.' your_script.py

In this command, 'libs\example.dll;.' tells PyInstaller to take example.dll from the libs directory and place it in the root directory of the bundled application (. signifies the root of your application's distribution folder).

Linux Example

For Linux, if you’re including a shared library (.so file), the process is similar. Assume you have a shared library called example.so in a libs directory. You would add it to your PyInstaller command like this:

pyinstaller --onefile --add-binary 'libs/example.so:.' your_script.py

Here, 'libs/example.so:.' instructs PyInstaller to include example.so from the libs directory into the root of the executable folder.

Adorning with Icons: A Touch of Personalization

Your script, now an application, deserves to stand out. A custom icon, perhaps? Consider app_with_icon.py, an application that aspires to be recognized.

Python Script (app_with_icon.py):

# app_with_icon.py

print("An icon of my own...")

Shell Command:

pyinstaller --onefile --icon=your_icon.ico app_with_icon.py

Debugging: Peering Behind the Curtain

Imagine you’ve developed a Python script that performs flawlessly in your development environment, but when bundled into an executable, it behaves unexpectedly. Such scenarios are where PyInstaller’s --debug flag becomes invaluable, acting as your flashlight in the murky depths of executable behavior.

Consider a Python script, mystery_behavior.py, which reads from a configuration file and logs messages based on that configuration. However, when converted into an executable, it mysteriously fails to read the configuration properly.

# mystery_behavior.py

import logging
import configparser

config = configparser.ConfigParser()
config.read('config.ini')

logging.basicConfig(level=logging.DEBUG)
logging.debug("Starting the application...")

if config['DEFAULT'].getboolean('debug_mode'):
logging.debug("Debug mode is enabled.")
else:
logging.debug("Debug mode is disabled.")

logging.debug("Performing tasks...")

In this script, everything seems straightforward, but the executable version doesn’t log messages as expected.

To peel back the layers and see what’s happening under the hood, you would use the --debug=all flag with PyInstaller. This flag instructs PyInstaller to provide verbose output during both the packaging process and the runtime of the executable, offering insights into where things might be going awry.

pyinstaller --onefile --debug=all mystery_behavior.py

By running the executable generated with this command, you can observe detailed logs that may indicate why the configuration file isn’t being read correctly. Perhaps the file path resolution behaves differently in the bundled environment, or there’s an issue with the way the executable accesses external files.

Packaging a GUI Application

For those scripts that paint windows and buttons, like simple_gui.py, crafted with the elegance of Tkinter. This example uses Tkinter for a basic GUI, so if you want to test this example, make sure you have Tkinter installed.

Python Script (simple_gui.py):

# simple_gui.py

import tkinter as tk

def main():
root = tk.Tk()
root.title("Simple GUI")
label = tk.Label(root, text="Hello, GUI World!")
label.pack()
root.mainloop()

if __name__ == "__main__":
main()

Shell Command:

pyinstaller --onefile --windowed simple_gui.py

The Spec File: A Blueprint for Customization

For the architects seeking to construct their executable with precision, the .spec file is your blueprint. This file, born from your first PyInstaller command, outlines the structure of your executable's package. Editing this file allows for meticulous customizations, from including data files in specific folders to setting that perfect icon that speaks your app's essence.

First, you need to generate a .spec file for your project. If you haven't done this already, run PyInstaller with your script:

pyinstaller your_script.py

This command creates a .spec file named after your script (e.g., your_script.spec) in the same directory.

To edit the .spec file you need to open the .spec file in a text editor. Here are some of the key areas you might customize:

  • pathex: Specifies additional paths for PyInstaller to search for Python modules and libraries during the compilation process, not at runtime. It’s about telling PyInstaller where to look to find all the Python code and native libraries your script imports. Use pathex if PyInstaller is not finding some of the Python modules or native libraries during the build process.
  • binaries: A list of non-Python files needed by the application, such as shared libraries and other binaries.
  • datas: Specifies additional data files to include in the bundle. Each tuple in the list contains the file’s source and the destination path relative to the app’s root.
  • hiddenimports: Lists imports that PyInstaller cannot detect automatically, ensuring these modules are included in the executable.
  • hookspath: Additional paths where PyInstaller should look for hooks. Hooks can tell PyInstaller about hidden imports and other details about specific packages.
  • runtime_hooks: Scripts that are run at runtime before any other code or module is imported. Useful for setting environment variables or patching the sys module.

Here’s an example modification that includes additional data files and sets a custom icon for a GUI application:

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

block_cipher = None

a = Analysis(['your_script.py'],
pathex=['path_to_your_script'],
binaries=[],
datas=[('path/to/data/files/*', 'data_files')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='your_application',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False , icon='path/to/your_icon.ico')

In this example, datas includes all files from path/to/data/files/ and places them in a folder named data_files within the application. The icon option for the EXE function sets a custom icon for the executable.

After editing the .spec file, run PyInstaller with the .spec file to rebuild your application:

pyinstaller your_script.spec

By editing the .spec file, developers can fine-tune how PyInstaller packages their Python application, from including data files and binaries to specifying custom icons and handling complex dependencies. This level of customization ensures that the standalone executable meets the specific requirements and behaviors of your application.

Conclusion

PyInstaller is a versatile tool that can transform your Python scripts into standalone executables with ease. Whether you’re a beginner looking to distribute your first Python application or an advanced user needing to package complex projects, PyInstaller offers a range of options to suit your needs. Remember to leverage the PyInstaller documentation for in-depth guidance on its vast array of features.

Your Support Means a Lot! 🙌

If you enjoyed this article and found it valuable, please consider giving it a clap to show your support. Feel free to explore my other articles, where I cover a wide range of topics related to Python programming and others. By following me, you’ll stay updated on my latest content and insights. I look forward to sharing more knowledge and connecting with you through future articles. Until then, keep coding, keep learning, and most importantly, enjoy the journey!

Happy programming!

References

--

--