pyi-doctor
Your PyInstaller exe works on your PC, then Windows Defender calls it a trojan on someone else's. pyi-doctor checks why.
In one call it builds a portable exe and a setup wizard, test-starts the build outside your Python environment, scores common antivirus false-positive risk factors, and lists the third-party licenses you are redistributing.
pip install pyi-doctor
pyi-doctor diagnose MyApp.exe
from pyi_doctor import Project, build_all
project = Project(entry="main.py", name="MyApp", version="1.0.0", publisher="Me")
report = build_all(project, log=print)
print(report.ok)
dist/release/
├── MyApp-1.0.0-portable-win64.zip # onedir build (+ THIRD_PARTY_LICENSES.txt)
├── MyApp-1.0.0-win64.exe # single-file build (if onefile = true)
├── MyApp-1.0.0-setup.exe # Inno Setup wizard (Windows + Inno Setup)
├── MyApp-1.0.0.cdx.json # CycloneDX 1.5 SBOM
├── THIRD_PARTY_LICENSES.txt
├── SHA256SUMS.txt
├── av-submission-notes.txt # text for false-positive submission forms
├── report.md / report.json
What it does
| Step | What happens |
|---|---|
| Portable build | PyInstaller onedir build (default) with --noupx, a Windows version resource and your icon. Optional onefile build. |
| Installer | Writes an Inno Setup script (fixed AppId for upgrades, per-user install by default, modern wizard, Start menu / optional desktop shortcut, uninstaller, English + Korean and other languages) and compiles it when Inno Setup 6.3+ is installed. |
| Smoke test | Copies each build to a temp folder and starts it with Python's environment variables and folders removed from the environment. Reports missing modules, data files and DLL errors with hints (e.g. "add 'x' to hidden_imports"). |
| Antivirus diagnosis | Reads the PE file (pure Python) and scores commonly cited risk factors: UPX sections, onefile, missing version resource, no signature, prebuilt bootloader, default icon. On Windows it can also scan with the local Microsoft Defender CLI (no remediation). |
| Licenses + SBOM | Maps bundled modules (from PyInstaller's TOC files) to installed distributions and writes THIRD_PARTY_LICENSES.txt (also placed next to the exe and inside the installer) and a CycloneDX SBOM. |
| Release folder | Collects the files, writes SHA256SUMS.txt, a report and false-positive submission notes. Optional upload to a GitHub release through the GitHub CLI (gh). |
Only the onedir build is required to succeed; every other step records its own status, so a missing tool (for example Inno Setup) does not stop the rest.
Tools such as briefcase and pyship focus on shipping an app. pyi-doctor focuses on the problems that tend to show up after shipping a PyInstaller build: "it works on my PC only", "Windows Defender calls it a trojan", and "which licenses am I redistributing?".
Why this exists
pyi-doctor was built by an elementary school student in Korea, with the help of AI coding tools, after his own PyInstaller apps kept getting flagged by antivirus software.
Install
pip install pyi-doctor
Requirements: Python 3.9+, PyInstaller 6+ (installed as a dependency). The installer step needs Windows and Inno Setup 6.3 or newer (not bundled; set PYI_DOCTOR_ISCC if it is not in the default location). The GitHub upload needs the GitHub CLI.
Build the Windows exe on Linux or macOS (experimental)
PyInstaller does not cross-compile, so a Windows exe normally needs a Windows machine (or Wine). pyi-doctor build --target win builds a PyInstaller-format Windows exe on Linux or macOS without Wine:
pip install pyi-doctor
pyi-doctor build --target win -r requirements.txt
from pyi_doctor import Project, build_windows
project = Project(entry="main.py", name="MyApp", version="1.0.0", requirements=["requests"])
onedir = build_windows(project) # dist/portable/MyApp/MyApp.exe (+ _internal)
single = build_windows(project, onefile=True) # dist/onefile/MyApp.exe
You get the onedir folder, an optional single-file exe, and an Inno Setup script with relative paths (dist/MyApp.iss) that compiles on Windows. For comparison, pynsist also builds on Linux/macOS but produces an installer only.
How it works:
- Downloads the python.org Windows embeddable package (
python3XY.dll, stdlib) for the same Python minor version as the build machine, thewin_amd64PyInstaller wheel of the installed PyInstaller version (Windows bootloader), andwin_amd64/ pure-Python wheels of your requirements (pip download, with Windows environment markers). Cached in~/.cache/pyi-doctor(PYI_DOCTOR_CACHE). - Finds modules with PyInstaller's modulegraph against those folders, and DLLs used by
.pydfiles from their PE import tables (pefile). PyInstaller hooks are run with replacements that read the unpacked files instead of importing the package. - Writes PYZ,
base_library.zipand the CArchive in PyInstaller's format, adds icon, version information and manifest to the bootloader as PE resources (pure Python), and sets the PE checksum. - Runs the antivirus risk-factor diagnosis on the result. It does not start the exe; examples/github-actions/cross-build-windows.yml builds on
ubuntu-latestand starts the exe onwindows-latest.
Scope. The build stops before writing files, with a list of reasons, when any of these apply:
- target other than 64-bit Windows (x64); no 32-bit or ARM64
- a requirement without a pure-Python or
win_amd64wheel for your Python version on PyPI - a PyInstaller hook that needs to import the package, defines
hook(hook_api), or calls a helper that cannot be answered from files - tkinter (and so customtkinter): the embeddable package has no Tcl/Tk
- a DLL needed by a
.pydthat is neither in the wheels nor a known Windows system DLL
Checked on Linux with Python 3.13 (the end-to-end test also passes on 3.12): stdlib (ssl, sqlite3, ctypes), requests, PyYAML (C extension), click, rich, tqdm, colorama, charset-normalizer (mypyc), Pillow, pydantic 2 (Rust extension), psutil, orjson and pywin32 built and ran under Wine 9 in onedir and onefile mode; numpy built, but could not be run under Wine 9. Other packages are untested. Results on real Windows can differ, so start the exe on Windows before distributing it.
Settings: requirements (defaults to [project].dependencies), -r FILE, --python-version X.Y.Z (must match the build Python's X.Y). pyinstaller_args and PNG icons are not supported by the cross build.
Configuration
Add a table to your pyproject.toml (pyi-doctor init --write adds a template):
[tool.pyi-doctor]
entry = "main.py"
name = "MyApp"
version = "1.0.0"
publisher = "Your Name"
description = "What the app does"
icon = "assets/app.ico"
app_id = "PUT-A-FIXED-GUID-HERE" # keep it fixed after the first release
console = false
onefile = false
license_file = "LICENSE" # shown in the installer
add_data = ["assets=assets"]
hidden_imports = []
languages = ["english", "korean"]
name, version and description fall back to [project]. If app_id is not set, a GUID is derived from publisher and name; changing either then changes the GUID, so set it explicitly before the first release.
All settings
| Key | Default | Meaning |
|---|---|---|
entry |
required | Script PyInstaller starts from |
name |
required | Exe / installer name |
version |
0.1.0 |
Version string; the first four numbers go into Windows version resources |
publisher, description, copyright, website |
"" |
Metadata for version resources, installer and reports |
icon |
none | .ico file |
app_id |
derived | Installer GUID |
console |
false |
true for command-line apps |
onefile |
false |
Also build a single-file exe |
upx |
false |
Allow UPX (adds a high-weight risk factor) |
hidden_imports, excludes |
[] |
Passed to PyInstaller |
add_data |
[] |
"src=dest", "src;dest" or ["src", "dest"] |
pyinstaller_args |
[] |
Extra PyInstaller arguments |
license_file |
none | License page in the installer |
privileges |
lowest |
lowest (per-user, can be raised in the wizard) or admin |
languages |
["english", "korean"] |
Installer languages available in your Inno Setup |
smoke_args, smoke_timeout |
[], 15 |
Arguments and seconds for the smoke test |
requirements |
[project].dependencies |
--target win only: requirement strings or "-r file.txt" for the Windows wheels |
output_dir, work_dir |
dist, build |
Output folders |
Command line
pyi-doctor build # full pipeline from pyproject.toml
pyi-doctor build --onefile --no-defender
pyi-doctor build --github-release v1.0.0 --github-repo me/myapp
pyi-doctor build --target win -r requirements.txt # Windows exe from Linux/macOS (experimental)
pyi-doctor diagnose dist/portable/MyApp/MyApp.exe # score any exe
pyi-doctor diagnose MyApp.exe --no-icon # also count the default-icon factor
pyi-doctor smoke dist/portable/MyApp/MyApp.exe -- --version
pyi-doctor iss # write the Inno Setup script only
pyi-doctor init --write # add [tool.pyi-doctor] to pyproject.toml
Library API
from pyi_doctor import Project, build_portable, build_installer, smoke_test, diagnose, collect_licenses
project = Project.from_pyproject("pyproject.toml")
onedir = build_portable(project) # Artifact(kind="onedir", path=..., folder=...)
result = smoke_test(onedir.path, timeout=10) # SmokeResult(ok, status, missing_modules, ...)
risk = diagnose(onedir.path, icon_configured=True) # Diagnosis(score, level, factors, defender, ...)
setup = build_installer(project, onedir.folder) # Windows + Inno Setup
packages, mode = collect_licenses(project.work_path / "onedir")
build_all(project, onefile=..., installer=..., smoke_test=..., diagnose=..., defender=..., licenses=..., sbom_file=..., github_tag=..., target="win") runs everything and returns a Report with .ok, .steps, .to_markdown() and .to_json().
About the antivirus score
The score adds fixed weights for factors that are commonly reported to affect heuristic detection of PyInstaller executables (UPX 30, onefile 20, no version resource 15, unsigned 15, prebuilt bootloader 10, default icon 5). It is meant for comparing your own builds. It does not predict the verdict of Microsoft Defender or any other product, and a low score does not mean a file will not be flagged.
pyi-doctor does not obfuscate, encrypt or otherwise hide code from scanners. It applies ordinary distribution practices (version information, no packer, onedir layout, signing advice) and helps you prepare a false-positive report. Code signing with a certificate from a trusted authority is generally the most effective measure and is outside the scope of this tool.
Measured on the bundled example
examples/hello, Linux x86_64, Python 3.13, PyInstaller 6.22.3 (installer step skipped because it needs Windows):
| Step | Time |
|---|---|
| onedir build | 4.5 s |
| onefile build | 12.2 s |
| licenses + SBOM | 0.8 s |
| smoke test (both builds) | 0.6 s |
| diagnosis | < 0.1 s |
| release folder (zip + checksums) | 1.5 s |
| total | 19.7 s |
Test suite: 167 tests (including 2 real PyInstaller builds and 2 cross builds) on Linux; CI runs the unit tests on Python 3.9–3.14 on Linux and Windows, and the cross build on Linux with a start test on Windows. Windows-specific paths (version resource, Inno Setup compilation, Defender scan, error-dialog detection in the smoke test) run in the Windows CI jobs.
Known limitations
- Windows features (installer, version resource, Defender scan) need Windows. On other systems the
.issscript is still written. - The smoke test is a heuristic. It removes Python from the environment but does not reproduce a clean PC (installed DLLs, Visual C++ runtime, fonts, etc.). GUI apps that keep running are counted as started. Windowed apps that crash show a PyInstaller error dialog; on Windows pyi-doctor looks for that dialog by window title, which may not catch every case.
- The bootloader check compares the installed PyInstaller bootloader with the hashes recorded in its wheel. It describes the PyInstaller in the current environment, not an exe built elsewhere.
- License notices come from package metadata, which can be missing or wrong; packages loaded dynamically may be missed. Review the generated file.
- Only PyInstaller is supported (no Nuitka / cx_Freeze). No code signing and no auto-update.
- The Windows cross build (
--target win) is experimental: it covers only the scope listed above, is not checked against every PyInstaller hook, and does not start the exe. Downloads from python.org and PyPI are not signature-checked beyond HTTPS.
Disclaimer
pyi-doctor is rule-based and may miss problems or report problems that do not exist. Its reports are informational and do not replace your own testing, review of licenses, or obligations under applicable law; checking the results and complying with license terms and laws remain the user's responsibility. The software is provided "AS IS", without warranty of any kind, under the MIT License.
pyi-doctor is an independent project and is not affiliated with or endorsed by the PyInstaller project, Jordan Russell / Inno Setup, Microsoft, or GitHub. Product names are trademarks of their respective owners.
License
MIT © 2026 Juwon Seo (서주원, OMEGA SYSTEMS) · Contact: seodes105@gmail.com
Metadata
Release files for pyi-doctor 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pyi_doctor-1.3.0.tar.gz | 88.3 kB | With provenance | Details for pyi_doctor-1.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pyi_doctor-1.3.0-py3-none-any.whl | Python 3 | none | any | Details for pyi_doctor-1.3.0-py3-none-any.whl |
Total release size: 165.6 kB
Release files / pyi_doctor-1.3.0.tar.gz
| Download URL | pyi_doctor-1.3.0.tar.gz |
|---|---|
| Size | 88.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f10ba4a988ded027adb78d10da970913a3238b0e590a7b112e6b4db579968d04
|
|
BLAKE2b-256 checksum How to use checksums |
74bd53ba0d59e9926240fb6ea1d819682b6c10886fd235929cd85978ae94e73d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logRelease files / pyi_doctor-1.3.0-py3-none-any.whl
| Download URL | pyi_doctor-1.3.0-py3-none-any.whl |
|---|---|
| Size | 77.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bf53e1e5274cf6bd101cfc06eeb278274d286893b76cd3b23f311f17d820702e
|
|
BLAKE2b-256 checksum How to use checksums |
cd2b72b4979f1d333a4f7adf6d75402b27fd695887d3dbd05f0bb477fd6a3fbc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logSecurity for pyi-doctor 1.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for pyi_doctor-1.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.