Of course! Using py2exe to handle DLLs is a common point of confusion, but it's manageable. The core idea is that py2exe tries to be smart and bundle the necessary DLLs automatically, but you often need to give it hints for external libraries, specific DLLs, or to control the output.

Here's a comprehensive guide covering the most common scenarios, from basic usage to advanced DLL handling.
The Basics: How py2exe Finds DLLs
By default, py2exe does a pretty good job:
- It scans your imported Python modules.
- It finds the C extensions (
.pydfiles, which are DLLs) that those modules depend on. - It bundles those
.pydand their required DLLs into yourdistfolder.
Example: A Simple Script with Pillow
Let's say you have a script that uses the Pillow library to generate an image.

app.py
import sys
import os
from PIL import Image, ImageDraw
def main():
# Create a simple image
img = Image.new('RGB', (200, 100), color = 'red')
d = ImageDraw.Draw(img)
d.text((10,10), "Hello py2exe!", fill=(255,255,0))
# Save the image
img.save("hello.png")
print("Image saved as hello.png")
print("Press Enter to exit...")
input()
if __name__ == "__main__":
main()
setup.py
from distutils.core import setup
import py2exe
setup(
name="MyApp",
version="0.1",
console=["app.py"], # This is a console application
)
To build:
python setup.py py2exe
After running this, you'll find a dist folder. Inside, you'll see app.exe, pythonXX.dll, and a library.zip containing your Python code. You'll also see DLLs related to Pillow, like libjpeg-9.dll, libpng16-16.dll, zlib1.dll, etc., because py2exe detected them as dependencies of the PIL module.

You can now run dist/app.exe on another Windows machine with Python installed, and it will work.
Common DLL Scenarios and Solutions
This is where you need to take more control. Here are the most frequent situations you'll encounter.
Scenario 1: Bundling an External DLL (e.g., a game asset library)
Imagine you have a custom DLL mygame_engine.dll that your Python script needs. py2exe won't know about it automatically.
Solution: Use the data_files option.
The data_files option is for including non-Python files that your application needs. It's the standard way to handle external DLLs, images, config files, etc.
setup.py (Modified)
from distutils.core import setup
import py2exe
# Define the path to your external DLL
# This assumes the DLL is in the same folder as setup.py
dll_path = "mygame_engine.dll"
setup(
name="MyApp",
version="0.1",
console=["app.py"],
# Use data_files to include the DLL
# The format is a list of tuples: (destination_directory, [list_of_files])
data_files=[("", [dll_path])], # "" means put it in the root of the dist folder
)
Now, when you run python setup.py py2exe, mygame_engine.dll will be copied directly into the dist folder alongside your app.exe.
Scenario 2: Forcing py2exe to Include a Specific DLL
Sometimes, a DLL is a dependency of a dependency, and py2exe's scanner might miss it. You can force it to be included.
Solution: Use the dll_excludes or includes option.
includes: Forcespy2exeto include a specific module (and its DLLs). This is the most common and recommended method.dll_excludes: Explicitly tellspy2exenot to bundle a specific DLL, which is useful for avoiding conflicts.
Example: Forcing inclusion of PyQt5's QtWebEngineCore.dll
If your PyQt5 app uses QtWebEngine, py2exe might not bundle all its DLLs by default. You can force it.
setup.py (Modified)
from distutils.core import setup
import py2exe
import sys
# A list of modules you want to ensure are included
# py2exe will find their dependencies automatically.
includes = [
'PyQt5.QtWebEngineCore', # The main module
'PyQt5.QtWebEngine', # Often needed as well
# Add any other specific modules from PyQt5 you use
]
setup(
name="MyPyQtApp",
version="0.1",
console=["app.py"],
options={"py2exe": {
"includes": includes,
# You can also exclude DLLs that cause problems
# "dll_excludes": ["MSVCP90.dll"], # Example: exclude a specific C runtime
}},
)
Scenario 3: Controlling DLL Output with bundle_files
By default, py2exe bundles all your Python code into a single library.zip file. However, the DLLs are placed loose in the dist folder. You can change this.
Solution: Use the bundle_files option.
bundle_files = 1: Puts everything (your code and all DLLs) into a single.exefile. This is very clean but can have a slight startup performance penalty and might be flagged by some antivirus as "packed".bundle_files = 3(default): Puts your code intolibrary.zipand DLLs loose in thedistfolder. This is the standard, most compatible method.
setup.py (Modified for a single .exe)
from distutils.core import setup
import py2exe
setup(
name="MyApp",
version="0.1",
console=["app.py"],
options={"py2exe": {
"bundle_files": 1, # Bundle everything into one .exe file
"compressed": 1, # Compress the library.zip
}},
)
Important Note: When using bundle_files = 1, you must also include the pythonXX.dll inside the executable. py2exe does this automatically.
Troubleshooting DLL Issues
Problem: The procedure entry point ... could not be located in ... DLL
This usually means a DLL is missing from the dist folder.
- Identify the missing DLL: Look at the error message. It will tell you which DLL is missing.
- Find where it lives: Search for the DLL on your development machine (e.g., using Windows Search).
- Bundle it:
- If it's a dependency of a Python module, use the
includeslist insetup.pyto force the inclusion of that module. - If it's a truly external DLL, use
data_filesto copy it into thedistfolder.
- If it's a dependency of a Python module, use the
Problem: Error loading ...dll or "Not a valid Win32 application"
This can happen if you mix 32-bit and 64-bit code.
- Check your Python: Is your Python interpreter 32-bit or 64-bit? (Run
python -c "import struct; print(struct.calcsize('P') * 8)"). - Check your DLLs: Right-click the DLL in the
distfolder -> Properties -> "Details" tab. Look at "File version" and "Original file name". A 64-bit DLL will often have "64" in its name or properties. - Consistency is key: Your Python, your script, and all your DLLs must be the same architecture (all 32-bit or all 64-bit).
Problem: Antivirus Flags Your .exe
This is extremely common because py2exe (especially with bundle_files=1) creates a packed executable.
- This is a false positive. The antivirus doesn't know what the executable does, only that it's compressed and contains many embedded DLLs.
- Solution: You can try to submit the file to the antivirus vendor as a false positive. For personal use, you might add an exception. For distribution, you may need to find a different packaging tool like PyInstaller or Nuitka, which sometimes produce less "suspicious" executables, or look into code-signing your application.
A Complete, Robust Example
Let's combine everything into a final setup.py for a complex application.
app.py
# This script uses a standard library (os) and an external library (requests)
import os
import requests
def main():
print("Hello from py2exe!")
print("Current working dir:", os.getcwd())
print("\nFetching data from httphttpbin.org...")
try:
response = requests.get('https://httpbin.org/get', timeout=5)
print(f"Status Code: {response.status_code}")
print(f"Response text (first 50 chars): {response.text[:50]}...")
except Exception as e:
print(f"An error occurred: {e}")
print("\nPress Enter to exit.")
input()
if __name__ == "__main__":
main()
setup.py
from distutils.core import setup
import py2exe
import sys
# --- Configuration ---
# 1. List of modules to explicitly include to ensure their DLLs are bundled.
# This is crucial for libraries like requests, numpy, PyQt, etc.
includes = [
'requests', # The requests library itself
'idna', # A dependency of requests
'chardet', # A dependency of requests
'certifi', # A dependency of requests
'urllib3', # A dependency of requests
# Add any other specific modules you use
]
# 2. List of DLLs to explicitly exclude (if any).
# This is useful if a DLL is causing a conflict or is already present
# on the target system (e.g., a specific C++ runtime).
dll_excludes = [
# 'MSVCP90.dll', # Example: exclude a problematic C runtime
]
# 3. List of files to bundle as data (e.g., config files, images, external DLLs).
# The format is a list of tuples: (destination_directory, [list_of_files])
# Use "" for the root of the dist folder.
data_files = [
# ("config", ["my_app_config.ini"]), # Example: put a config file in a 'config' subdir
# ("", ["my_custom_library.dll"]), # Example: put a DLL in the root
]
# --- Setup Command ---
setup(
name="MyRobustApp",
version="1.0",
description="A robust py2exe example.",
console=["app.py"], # Use ['app.py'] for a console app, or 'windows' for a GUI app
# py2exe specific options
options={
"py2exe": {
# Force inclusion of modules
"includes": includes,
# Exclude specific DLLs
"dll_excludes": dll_excludes,
# Controls how Python code is bundled.
# 1 = single .exe, 3 = .exe + library.zip (default)
"bundle_files": 3,
# Compress the library.zip file
"compressed": 1,
# Specify a custom icon for the .exe
# "icon_resources": [(1, "my_icon.ico")]
}
},
# Bundle non-Python files
data_files=data_files,
)
Final Recommendation
While py2exe is a classic and powerful tool, many developers today prefer PyInstaller. It's often easier to use, has better support for modern Python packages, and produces fewer "false positive" antivirus alerts. The principles are similar (includes, excludes, data files), but the syntax in the spec file or command-line arguments can be more straightforward.
However, if you're working on an older codebase or have specific needs, py2exe is still a perfectly viable solution.
