[{"data":1,"prerenderedAt":1463},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller":1455},{"id":4,"title":5,"body":6,"dateModified":1425,"datePublished":1425,"description":1426,"extension":1427,"faq":1428,"meta":1437,"navigation":735,"path":1447,"seo":1448,"slug":1451,"stem":1452,"type":1453,"__hash__":1454},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller\u002Findex.md","Package a Python Excel Script as an EXE with PyInstaller",{"type":7,"value":8,"toc":1412},"minimark",[9,18,27,125,130,160,163,176,180,183,206,217,238,245,249,256,343,470,481,484,688,695,699,706,955,964,1046,1050,1053,1060,1066,1077,1081,1091,1133,1141,1145,1274,1278,1281,1316,1319,1323,1326,1330,1337,1343,1349,1362,1366,1369,1378,1381,1408],[10,11,12,13,17],"p",{},"There is one situation where a packaged executable is the right answer: the person who needs the report has a locked-down desktop, no Python, no ability to install one, and the job must run there rather than on a server. In every other case a virtual environment or a ",[14,15,16],"code",{},"pipx install"," is smaller, faster to fix and easier to update.",[10,19,20,21,26],{},"If you are in that situation, PyInstaller does the job well — with two complications specific to Excel work. pandas and openpyxl pull in modules that PyInstaller's static analysis cannot see, and a report usually needs files (a template workbook, a config file, a logo) that must either travel inside the bundle or sit beside it. This guide, part of ",[22,23,25],"a",{"href":24},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",", covers both.",[28,29,37,38,37,42,37,46,37,53,37,63,37,70,37,75,37,79,37,83,37,88,37,92,37,97,37,102,37,105,37,108,37,111,37,114,37,117],"svg",{"viewBox":30,"role":31,"ariaLabelledBy":32,"xmlns":35,"style":36},"0 0 740 244","img",[33,34],"pi-shape-t","pi-shape-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:740px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[39,40,41],"title",{"id":33},"What goes inside the bundle and what stays beside it",[43,44,45],"desc",{"id":34},"The executable contains the Python interpreter, pandas and openpyxl, your code, and read-only assets such as a template workbook and a logo. The config file, the input export and the generated reports stay in the folder next to the executable, where the user can see and edit them.",[47,48],"rect",{"x":49,"y":49,"width":50,"height":51,"fill":52},"0","740","244","#ffffff",[47,54],{"x":55,"y":56,"width":57,"height":58,"rx":59,"fill":60,"stroke":61,"style":62},"20","34","330","188","14","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[64,65,69],"text",{"x":66,"y":67,"style":68},"185","62","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","inside report.exe",[64,71,74],{"x":66,"y":72,"style":73},"92","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","the Python interpreter",[64,76,78],{"x":66,"y":77,"style":73},"114","pandas · openpyxl · your code",[64,80,82],{"x":66,"y":81,"style":73},"136","template.xlsx, logo.png",[64,84,87],{"x":66,"y":85,"style":86},"172","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","read-only — changing any of it",[64,89,91],{"x":66,"y":90,"style":86},"190","means a rebuild and a redistribution",[47,93],{"x":94,"y":56,"width":57,"height":58,"rx":59,"fill":95,"stroke":96,"style":62},"390","#d9f4f1","var(--teal,#0f9488)",[64,98,101],{"x":99,"y":67,"style":100},"555","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","beside report.exe",[64,103,104],{"x":99,"y":72,"style":73},"config.toml",[64,106,107],{"x":99,"y":77,"style":73},"the input export",[64,109,110],{"x":99,"y":81,"style":73},"the reports it writes",[64,112,113],{"x":99,"y":85,"style":86},"editable — the user changes a recipient",[64,115,116],{"x":99,"y":90,"style":86},"without you rebuilding anything",[118,119],"line",{"x1":120,"y1":121,"x2":122,"y2":121,"stroke":123,"style":124},"354","128","384","var(--muted,#5b6780)","stroke-width:2px;stroke-dasharray:4 3",[126,127,129],"h2",{"id":128},"prerequisites","Prerequisites",[131,132,137],"pre",{"className":133,"code":134,"language":135,"meta":136,"style":136},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pyinstaller pandas openpyxl\n","bash","",[14,138,139],{"__ignoreMap":136},[140,141,143,147,151,154,157],"span",{"class":118,"line":142},1,[140,144,146],{"class":145},"sMTad","pip",[140,148,150],{"class":149},"srMev"," install",[140,152,153],{"class":149}," pyinstaller",[140,155,156],{"class":149}," pandas",[140,158,159],{"class":149}," openpyxl\n",[10,161,162],{},"Build on the operating system you are shipping to — PyInstaller freezes the interpreter of the machine it runs on, so there is no cross-compilation. Build inside a virtual environment containing only what the script needs; a build run from a general-purpose environment sweeps in every library you have ever installed, and the executable grows accordingly.",[10,164,165,166,170,171,175],{},"The script should already have a ",[22,167,169],{"href":168},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse\u002F","command line"," and ",[22,172,174],{"href":173},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","external configuration",", because both become much harder to add once the code is inside a bundle.",[126,177,179],{"id":178},"step-1-the-first-build","Step 1: The first build",[10,181,182],{},"Start with the simplest command that produces something to test:",[131,184,186],{"className":133,"code":185,"language":135,"meta":136,"style":136},"pyinstaller --onefile --name report cli.py\n",[14,187,188],{"__ignoreMap":136},[140,189,190,193,197,200,203],{"class":118,"line":142},[140,191,192],{"class":145},"pyinstaller",[140,194,196],{"class":195},"sP0c6"," --onefile",[140,198,199],{"class":195}," --name",[140,201,202],{"class":149}," report",[140,204,205],{"class":149}," cli.py\n",[10,207,208,209,212,213,216],{},"The result lands in ",[14,210,211],{},"dist\u002Freport.exe"," (or ",[14,214,215],{},"dist\u002Freport"," on Linux and macOS). Run it immediately with the same arguments a user would:",[131,218,220],{"className":133,"code":219,"language":135,"meta":136,"style":136},"dist\u002Freport.exe orders.xlsx --output july.xlsx --verbose\n",[14,221,222],{"__ignoreMap":136},[140,223,224,226,229,232,235],{"class":118,"line":142},[140,225,211],{"class":145},[140,227,228],{"class":149}," orders.xlsx",[140,230,231],{"class":195}," --output",[140,233,234],{"class":149}," july.xlsx",[140,236,237],{"class":195}," --verbose\n",[10,239,240,241,244],{},"If it works, you are most of the way there. If it fails, the error is almost always a ",[14,242,243],{},"ModuleNotFoundError"," for something you never imported directly — which is the next step.",[126,246,248],{"id":247},"step-2-fix-the-imports-pyinstaller-cannot-see","Step 2: Fix the imports PyInstaller cannot see",[10,250,251,252,255],{},"PyInstaller finds dependencies by reading the source for ",[14,253,254],{},"import"," statements. pandas chooses its Excel engine at runtime by name, so nothing in your code mentions openpyxl and the analysis misses it. The same happens with several pandas internals:",[131,257,259],{"className":133,"code":258,"language":135,"meta":136,"style":136},"pyinstaller --onefile --name report \\\n  --hidden-import openpyxl \\\n  --hidden-import openpyxl.cell._writer \\\n  --hidden-import pandas._libs.tslibs.base \\\n  --exclude-module matplotlib \\\n  --exclude-module tkinter \\\n  --exclude-module pytest \\\n  cli.py\n",[14,260,261,275,286,296,306,317,327,337],{"__ignoreMap":136},[140,262,263,265,267,269,271],{"class":118,"line":142},[140,264,192],{"class":145},[140,266,196],{"class":195},[140,268,199],{"class":195},[140,270,202],{"class":149},[140,272,274],{"class":273},"sSjpA"," \\\n",[140,276,278,281,284],{"class":118,"line":277},2,[140,279,280],{"class":195},"  --hidden-import",[140,282,283],{"class":149}," openpyxl",[140,285,274],{"class":273},[140,287,289,291,294],{"class":118,"line":288},3,[140,290,280],{"class":195},[140,292,293],{"class":149}," openpyxl.cell._writer",[140,295,274],{"class":273},[140,297,299,301,304],{"class":118,"line":298},4,[140,300,280],{"class":195},[140,302,303],{"class":149}," pandas._libs.tslibs.base",[140,305,274],{"class":273},[140,307,309,312,315],{"class":118,"line":308},5,[140,310,311],{"class":195},"  --exclude-module",[140,313,314],{"class":149}," matplotlib",[140,316,274],{"class":273},[140,318,320,322,325],{"class":118,"line":319},6,[140,321,311],{"class":195},[140,323,324],{"class":149}," tkinter",[140,326,274],{"class":273},[140,328,330,332,335],{"class":118,"line":329},7,[140,331,311],{"class":195},[140,333,334],{"class":149}," pytest",[140,336,274],{"class":273},[140,338,340],{"class":118,"line":339},8,[140,341,342],{"class":149},"  cli.py\n",[28,344,37,349,37,352,37,355,37,358,37,364,37,370,37,374,37,379,37,384,37,390,37,393,37,398,37,401,37,407,37,411,37,416,37,420,37,425,37,429,37,433,37,437,37,443,37,448,37,451,37,460,37,466],{"viewBox":345,"role":31,"ariaLabelledBy":346,"xmlns":35,"style":36},"0 24 740 190",[347,348],"pi-hidden-t","pi-hidden-d",[39,350,351],{"id":347},"Why PyInstaller misses the Excel engine",[43,353,354],{"id":348},"PyInstaller finds dependencies by reading import statements. Your code imports pandas, and pandas imports openpyxl only at runtime by looking the engine up by name, so the static analysis never sees it and the bundle ships without it. Naming it as a hidden import, or importing it explicitly yourself, closes the gap.",[47,356],{"x":49,"y":357,"width":50,"height":90,"fill":52},"24",[47,359],{"x":357,"y":360,"width":361,"height":362,"rx":363,"fill":60,"stroke":61,"style":62},"52","150","56","10",[64,365,369],{"x":366,"y":367,"style":368},"99","76","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","your code",[64,371,373],{"x":366,"y":372,"style":86},"96","import pandas",[118,375],{"x1":376,"y1":377,"x2":378,"y2":377,"stroke":96,"style":62},"178","80","212",[380,381],"polygon",{"points":382,"fill":383},"216,80 206,75 206,85","#0f766e",[64,385,389],{"x":386,"y":387,"style":388},"197","42","font-size:10px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","seen",[47,391],{"x":392,"y":360,"width":361,"height":362,"rx":363,"fill":95,"stroke":96,"style":62},"220",[64,394,397],{"x":395,"y":367,"style":396},"295","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","pandas",[64,399,400],{"x":395,"y":372,"style":86},"bundled",[118,402],{"x1":403,"y1":377,"x2":404,"y2":377,"stroke":405,"style":406},"374","408","var(--accent-ink,#be185d)","stroke-width:2px;stroke-dasharray:5 4",[380,408],{"points":409,"fill":410},"412,80 402,75 402,85","#d81b73",[64,412,415],{"x":413,"y":387,"style":414},"393","font-size:10px;font-weight:600;fill:var(--accent-ink,#be185d);text-anchor:middle","by name",[47,417],{"x":418,"y":360,"width":361,"height":362,"rx":363,"fill":419,"stroke":405,"style":62},"416","#fce9e9",[64,421,424],{"x":422,"y":367,"style":423},"491","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","openpyxl",[64,426,428],{"x":422,"y":372,"style":427},"font-size:11px;fill:var(--text,#172033);text-anchor:middle","never bundled",[118,430],{"x1":431,"y1":377,"x2":432,"y2":377,"stroke":123,"style":62},"570","604",[380,434],{"points":435,"fill":436},"608,80 598,75 598,85","#5b6780",[47,438],{"x":439,"y":360,"width":440,"height":362,"rx":363,"fill":441,"stroke":442,"style":62},"612","112","#f0f2f5","var(--line,#cdd5e6)",[64,444,447],{"x":445,"y":367,"style":446},"668","font-size:11px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","first run:",[64,449,450],{"x":445,"y":372,"style":86},"ModuleNotFound",[47,452],{"x":453,"y":454,"width":455,"height":456,"rx":457,"fill":458,"stroke":459,"style":62},"120","132","500","66","12","#fdefd8","var(--gold,#b4740a)",[64,461,465],{"x":462,"y":463,"style":464},"370","158","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Two ways to close the dashed arrow",[64,467,469],{"x":462,"y":468,"style":427},"180","--hidden-import openpyxl in the spec, or a plain import openpyxl in your own module",[10,471,472,473,476,477,480],{},"Adding an explicit ",[14,474,475],{},"import openpyxl"," at the top of your own module fixes the first one just as well, and is easier to remember. The ",[14,478,479],{},"--exclude-module"," flags are the cheapest size win available: matplotlib and tkinter are pulled in by transitive dependencies far more often than they are used, and removing them typically takes 20-30 MB off the bundle.",[10,482,483],{},"When a module still goes missing at runtime, the error names it exactly — add it to the hidden-import list and rebuild. Keep the growing command in a spec file rather than a shell history:",[131,485,489],{"className":486,"code":487,"language":488,"meta":136,"style":136},"language-python shiki shiki-themes github-light github-dark-high-contrast","# report.spec — generated by the command above, then edited and committed\na = Analysis(\n    [\"cli.py\"],\n    pathex=[],\n    binaries=[],\n    datas=[(\"assets\u002Ftemplate.xlsx\", \"assets\"),      # (source, destination in bundle)\n           (\"assets\u002Flogo.png\", \"assets\")],\n    hiddenimports=[\"openpyxl\", \"openpyxl.cell._writer\"],\n    excludes=[\"matplotlib\", \"tkinter\", \"pytest\"],\n)\npyz = PYZ(a.pure)\nexe = EXE(pyz, a.scripts, a.binaries, a.datas, name=\"report\",\n          console=True, upx=False)\n","python",[14,490,491,497,510,521,532,541,566,581,601,626,632,643,665],{"__ignoreMap":136},[140,492,493],{"class":118,"line":142},[140,494,496],{"class":495},"s-wDw","# report.spec — generated by the command above, then edited and committed\n",[140,498,499,503,507],{"class":118,"line":277},[140,500,502],{"class":501},"skGVy","a ",[140,504,506],{"class":505},"s-kum","=",[140,508,509],{"class":501}," Analysis(\n",[140,511,512,515,518],{"class":118,"line":288},[140,513,514],{"class":501},"    [",[140,516,517],{"class":149},"\"cli.py\"",[140,519,520],{"class":501},"],\n",[140,522,523,527,529],{"class":118,"line":298},[140,524,526],{"class":525},"sa561","    pathex",[140,528,506],{"class":505},[140,530,531],{"class":501},"[],\n",[140,533,534,537,539],{"class":118,"line":308},[140,535,536],{"class":525},"    binaries",[140,538,506],{"class":505},[140,540,531],{"class":501},[140,542,543,546,548,551,554,557,560,563],{"class":118,"line":319},[140,544,545],{"class":525},"    datas",[140,547,506],{"class":505},[140,549,550],{"class":501},"[(",[140,552,553],{"class":149},"\"assets\u002Ftemplate.xlsx\"",[140,555,556],{"class":501},", ",[140,558,559],{"class":149},"\"assets\"",[140,561,562],{"class":501},"),      ",[140,564,565],{"class":495},"# (source, destination in bundle)\n",[140,567,568,571,574,576,578],{"class":118,"line":329},[140,569,570],{"class":501},"           (",[140,572,573],{"class":149},"\"assets\u002Flogo.png\"",[140,575,556],{"class":501},[140,577,559],{"class":149},[140,579,580],{"class":501},")],\n",[140,582,583,586,588,591,594,596,599],{"class":118,"line":339},[140,584,585],{"class":525},"    hiddenimports",[140,587,506],{"class":505},[140,589,590],{"class":501},"[",[140,592,593],{"class":149},"\"openpyxl\"",[140,595,556],{"class":501},[140,597,598],{"class":149},"\"openpyxl.cell._writer\"",[140,600,520],{"class":501},[140,602,604,607,609,611,614,616,619,621,624],{"class":118,"line":603},9,[140,605,606],{"class":525},"    excludes",[140,608,506],{"class":505},[140,610,590],{"class":501},[140,612,613],{"class":149},"\"matplotlib\"",[140,615,556],{"class":501},[140,617,618],{"class":149},"\"tkinter\"",[140,620,556],{"class":501},[140,622,623],{"class":149},"\"pytest\"",[140,625,520],{"class":501},[140,627,629],{"class":118,"line":628},10,[140,630,631],{"class":501},")\n",[140,633,635,638,640],{"class":118,"line":634},11,[140,636,637],{"class":501},"pyz ",[140,639,506],{"class":505},[140,641,642],{"class":501}," PYZ(a.pure)\n",[140,644,646,649,651,654,657,659,662],{"class":118,"line":645},12,[140,647,648],{"class":501},"exe ",[140,650,506],{"class":505},[140,652,653],{"class":501}," EXE(pyz, a.scripts, a.binaries, a.datas, ",[140,655,656],{"class":525},"name",[140,658,506],{"class":505},[140,660,661],{"class":149},"\"report\"",[140,663,664],{"class":501},",\n",[140,666,668,671,673,676,678,681,683,686],{"class":118,"line":667},13,[140,669,670],{"class":525},"          console",[140,672,506],{"class":505},[140,674,675],{"class":195},"True",[140,677,556],{"class":501},[140,679,680],{"class":525},"upx",[140,682,506],{"class":505},[140,684,685],{"class":195},"False",[140,687,631],{"class":501},[10,689,690,691,694],{},"Then build with ",[14,692,693],{},"pyinstaller report.spec",". The spec file is the build definition; committing it means the next person rebuilds exactly what you shipped.",[126,696,698],{"id":697},"step-3-find-your-own-files-at-runtime","Step 3: Find your own files at runtime",[10,700,701,702,705],{},"A frozen application has two directories that matter, and they are not the same one. Bundled assets are unpacked into a temporary folder that PyInstaller records in ",[14,703,704],{},"sys._MEIPASS","; the user's editable files live next to the executable. Getting these the wrong way round is the most common packaging bug in report tools:",[131,707,709],{"className":486,"code":708,"language":488,"meta":136,"style":136},"import sys\nfrom pathlib import Path\n\n\ndef bundled(relative):\n    \"\"\"A read-only asset shipped INSIDE the executable (template, logo).\"\"\"\n    base = Path(getattr(sys, \"_MEIPASS\", Path(__file__).parent))\n    return base \u002F relative\n\n\ndef alongside(relative):\n    \"\"\"An editable file NEXT TO the executable (config, output directory).\"\"\"\n    if getattr(sys, \"frozen\", False):\n        base = Path(sys.executable).parent\n    else:\n        base = Path(__file__).parent\n    return base \u002F relative\n\n\nTEMPLATE = bundled(\"assets\u002Ftemplate.xlsx\")     # read-only, inside\nCONFIG = alongside(\"config.toml\")              # editable, outside\nOUTPUT_DIR = alongside(\"reports\")              # the user can open this folder\n",[14,710,711,718,731,737,741,753,758,786,800,804,808,817,822,842,853,862,876,887,892,897,917,937],{"__ignoreMap":136},[140,712,713,715],{"class":118,"line":142},[140,714,254],{"class":505},[140,716,717],{"class":501}," sys\n",[140,719,720,723,726,728],{"class":118,"line":277},[140,721,722],{"class":505},"from",[140,724,725],{"class":501}," pathlib ",[140,727,254],{"class":505},[140,729,730],{"class":501}," Path\n",[140,732,733],{"class":118,"line":288},[140,734,736],{"emptyLinePlaceholder":735},true,"\n",[140,738,739],{"class":118,"line":298},[140,740,736],{"emptyLinePlaceholder":735},[140,742,743,746,750],{"class":118,"line":308},[140,744,745],{"class":505},"def",[140,747,749],{"class":748},"s_Opv"," bundled",[140,751,752],{"class":501},"(relative):\n",[140,754,755],{"class":118,"line":319},[140,756,757],{"class":149},"    \"\"\"A read-only asset shipped INSIDE the executable (template, logo).\"\"\"\n",[140,759,760,763,765,768,771,774,777,780,783],{"class":118,"line":329},[140,761,762],{"class":501},"    base ",[140,764,506],{"class":505},[140,766,767],{"class":501}," Path(",[140,769,770],{"class":195},"getattr",[140,772,773],{"class":501},"(sys, ",[140,775,776],{"class":149},"\"_MEIPASS\"",[140,778,779],{"class":501},", Path(",[140,781,782],{"class":195},"__file__",[140,784,785],{"class":501},").parent))\n",[140,787,788,791,794,797],{"class":118,"line":339},[140,789,790],{"class":505},"    return",[140,792,793],{"class":501}," base ",[140,795,796],{"class":505},"\u002F",[140,798,799],{"class":501}," relative\n",[140,801,802],{"class":118,"line":603},[140,803,736],{"emptyLinePlaceholder":735},[140,805,806],{"class":118,"line":628},[140,807,736],{"emptyLinePlaceholder":735},[140,809,810,812,815],{"class":118,"line":634},[140,811,745],{"class":505},[140,813,814],{"class":748}," alongside",[140,816,752],{"class":501},[140,818,819],{"class":118,"line":645},[140,820,821],{"class":149},"    \"\"\"An editable file NEXT TO the executable (config, output directory).\"\"\"\n",[140,823,824,827,830,832,835,837,839],{"class":118,"line":667},[140,825,826],{"class":505},"    if",[140,828,829],{"class":195}," getattr",[140,831,773],{"class":501},[140,833,834],{"class":149},"\"frozen\"",[140,836,556],{"class":501},[140,838,685],{"class":195},[140,840,841],{"class":501},"):\n",[140,843,845,848,850],{"class":118,"line":844},14,[140,846,847],{"class":501},"        base ",[140,849,506],{"class":505},[140,851,852],{"class":501}," Path(sys.executable).parent\n",[140,854,856,859],{"class":118,"line":855},15,[140,857,858],{"class":505},"    else",[140,860,861],{"class":501},":\n",[140,863,865,867,869,871,873],{"class":118,"line":864},16,[140,866,847],{"class":501},[140,868,506],{"class":505},[140,870,767],{"class":501},[140,872,782],{"class":195},[140,874,875],{"class":501},").parent\n",[140,877,879,881,883,885],{"class":118,"line":878},17,[140,880,790],{"class":505},[140,882,793],{"class":501},[140,884,796],{"class":505},[140,886,799],{"class":501},[140,888,890],{"class":118,"line":889},18,[140,891,736],{"emptyLinePlaceholder":735},[140,893,895],{"class":118,"line":894},19,[140,896,736],{"emptyLinePlaceholder":735},[140,898,900,903,906,909,911,914],{"class":118,"line":899},20,[140,901,902],{"class":195},"TEMPLATE",[140,904,905],{"class":505}," =",[140,907,908],{"class":501}," bundled(",[140,910,553],{"class":149},[140,912,913],{"class":501},")     ",[140,915,916],{"class":495},"# read-only, inside\n",[140,918,920,923,925,928,931,934],{"class":118,"line":919},21,[140,921,922],{"class":195},"CONFIG",[140,924,905],{"class":505},[140,926,927],{"class":501}," alongside(",[140,929,930],{"class":149},"\"config.toml\"",[140,932,933],{"class":501},")              ",[140,935,936],{"class":495},"# editable, outside\n",[140,938,940,943,945,947,950,952],{"class":118,"line":939},22,[140,941,942],{"class":195},"OUTPUT_DIR",[140,944,905],{"class":505},[140,946,927],{"class":501},[140,948,949],{"class":149},"\"reports\"",[140,951,933],{"class":501},[140,953,954],{"class":495},"# the user can open this folder\n",[10,956,957,960,961,963],{},[14,958,959],{},"sys.frozen"," is set only in a packaged build, so both helpers work unchanged when you run the script normally during development. Writing anywhere inside ",[14,962,704],{}," is the failure to avoid: that directory is deleted when the process exits, so a report saved there vanishes the moment the job finishes — and on a one-file build it is recreated on every run, so nothing persists between them.",[28,965,37,970,37,973,37,976,37,979,37,987,37,992,37,999,37,1002,37,1006,37,1009,37,1014,37,1019,37,1023,37,1027,37,1030,37,1033,37,1036,37,1039,37,1043],{"viewBox":966,"role":31,"ariaLabelledBy":967,"xmlns":35,"style":36},"0 0 740 250",[968,969],"pi-paths-t","pi-paths-d",[39,971,972],{"id":968},"Where a frozen script should read from and where it must write to",[43,974,975],{"id":969},"On startup a one-file build unpacks its bundled assets into a temporary directory recorded in sys._MEIPASS, which is deleted when the process exits. Templates and logos are read from there. The config file, the input and the generated reports must live in the directory containing the executable, which survives between runs.",[47,977],{"x":49,"y":49,"width":50,"height":978,"fill":52},"250",[47,980],{"x":981,"y":982,"width":983,"height":984,"rx":457,"fill":985,"stroke":986,"style":62},"266","22","208","44","#5b5cf0","var(--brand-strong,#4338ca)",[64,988,991],{"x":462,"y":989,"style":990},"50","font-size:12.5px;font-weight:700;fill:#ffffff;text-anchor:middle","report.exe starts",[118,993],{"x1":994,"y1":995,"x2":996,"y2":997,"stroke":123,"style":998},"320","68","200","104","stroke-width:1.5px",[380,1000],{"points":1001,"fill":436},"196,106 208,100 206,110",[118,1003],{"x1":1004,"y1":995,"x2":1005,"y2":997,"stroke":123,"style":998},"420","540",[380,1007],{"points":1008,"fill":436},"544,106 532,100 534,110",[47,1010],{"x":357,"y":1011,"width":1012,"height":1013,"rx":59,"fill":458,"stroke":459,"style":62},"110","322","118",[64,1015,1018],{"x":66,"y":1016,"style":1017},"138","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","sys._MEIPASS — temporary",[64,1020,1022],{"x":66,"y":1021,"style":73},"164","READ template.xlsx, logo.png",[64,1024,1026],{"x":66,"y":58,"style":1025},"font-size:11.5px;fill:var(--accent-ink,#be185d);text-anchor:middle","never write here",[64,1028,1029],{"x":66,"y":378,"style":86},"deleted when the process exits",[47,1031],{"x":1032,"y":1011,"width":1012,"height":1013,"rx":59,"fill":95,"stroke":96,"style":62},"394",[64,1034,1035],{"x":99,"y":1016,"style":100},"Path(sys.executable).parent",[64,1037,1038],{"x":99,"y":1021,"style":73},"READ config.toml, the export",[64,1040,1042],{"x":99,"y":58,"style":1041},"font-size:11.5px;fill:var(--teal-ink,#0b6157);text-anchor:middle","WRITE reports\u002F and the log",[64,1044,1045],{"x":99,"y":378,"style":86},"survives between runs, visible to the user",[126,1047,1049],{"id":1048},"step-4-ship-a-folder-not-a-file","Step 4: Ship a folder, not a file",[10,1051,1052],{},"What the user receives should be a small folder they can drop anywhere:",[131,1054,1058],{"className":1055,"code":1057,"language":64,"meta":136},[1056],"language-text","regional-report\u002F\n├── report.exe\n├── config.toml          ← they edit this\n├── README.txt           ← three lines: what to edit, how to run, who to ask\n└── reports\u002F             ← output lands here\n",[14,1059,1057],{"__ignoreMap":136},[10,1061,1062,1065],{},[14,1063,1064],{},"README.txt"," is not a formality. The three things a non-technical user needs are which file to edit, the exact command or double-click that runs it, and who to contact — and a text file beside the executable is the only documentation that reliably travels with it.",[10,1067,1068,1069,1072,1073,1076],{},"Prefer ",[14,1070,1071],{},"--onedir"," over ",[14,1074,1075],{},"--onefile"," for this audience despite the extra files. A one-file build unpacks the whole bundle to a temporary directory on every launch, which adds several seconds of startup for a pandas-based tool and is a frequent trigger for antivirus heuristics. One-directory starts immediately and looks more like ordinary software.",[126,1078,1080],{"id":1079},"step-5-verify-the-build-like-a-user","Step 5: Verify the build like a user",[10,1082,1083,1084,170,1087,1090],{},"Test on a machine that has never had Python installed, or at minimum in a clean environment with ",[14,1085,1086],{},"PATH",[14,1088,1089],{},"PYTHONPATH"," cleared. A build that quietly imports a library from your development machine passes every test you run and fails on the first desktop it reaches:",[131,1092,1094],{"className":133,"code":1093,"language":135,"meta":136,"style":136},"# from a clean directory containing only the shipped folder\ncd \u002Ftmp\u002Fhandover\u002Fregional-report\n.\u002Freport.exe sample-export.xlsx --verbose\necho \"exit code: $?\"\n",[14,1095,1096,1101,1109,1119],{"__ignoreMap":136},[140,1097,1098],{"class":118,"line":142},[140,1099,1100],{"class":495},"# from a clean directory containing only the shipped folder\n",[140,1102,1103,1106],{"class":118,"line":277},[140,1104,1105],{"class":195},"cd",[140,1107,1108],{"class":149}," \u002Ftmp\u002Fhandover\u002Fregional-report\n",[140,1110,1111,1114,1117],{"class":118,"line":288},[140,1112,1113],{"class":145},".\u002Freport.exe",[140,1115,1116],{"class":149}," sample-export.xlsx",[140,1118,237],{"class":195},[140,1120,1121,1124,1127,1130],{"class":118,"line":298},[140,1122,1123],{"class":195},"echo",[140,1125,1126],{"class":149}," \"exit code: ",[140,1128,1129],{"class":195},"$?",[140,1131,1132],{"class":149},"\"\n",[10,1134,1135,1136,1140],{},"Check the exit code as well as the output file, because the ",[22,1137,1139],{"href":1138},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-python-excel-script-on-windows-task-scheduler\u002F","scheduler"," on the user's machine reads that number and nothing else.",[126,1142,1144],{"id":1143},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1146,1147,1148,1164],"table",{},[1149,1150,1151],"thead",{},[1152,1153,1154,1158,1161],"tr",{},[1155,1156,1157],"th",{},"Symptom",[1155,1159,1160],{},"Cause",[1155,1162,1163],{},"Fix",[1165,1166,1167,1185,1200,1211,1224,1237,1248,1259],"tbody",{},[1152,1168,1169,1176,1179],{},[1170,1171,1172,1175],"td",{},[14,1173,1174],{},"ModuleNotFoundError: openpyxl"," at runtime",[1170,1177,1178],{},"pandas selects the engine by name; the analysis missed it",[1170,1180,1181,1184],{},[14,1182,1183],{},"--hidden-import openpyxl",", or import it explicitly in your code",[1152,1186,1187,1190,1195],{},[1170,1188,1189],{},"Report file vanishes after the run",[1170,1191,1192,1193],{},"Written inside ",[14,1194,704],{},[1170,1196,1197,1198],{},"Write to ",[14,1199,1035],{},[1152,1201,1202,1205,1208],{},[1170,1203,1204],{},"Executable is 100 MB+",[1170,1206,1207],{},"Built from a general-purpose environment",[1170,1209,1210],{},"Build in a clean venv; exclude matplotlib, tkinter, tests",[1152,1212,1213,1216,1219],{},[1170,1214,1215],{},"Slow start, then it works",[1170,1217,1218],{},"One-file build unpacking every launch",[1170,1220,1221,1222],{},"Use ",[14,1223,1071],{},[1152,1225,1226,1229,1232],{},[1170,1227,1228],{},"Antivirus quarantines the file",[1170,1230,1231],{},"Unsigned self-extracting binary",[1170,1233,1234,1236],{},[14,1235,1071],{},", and sign the executable if possible",[1152,1238,1239,1242,1245],{},[1170,1240,1241],{},"Config changes have no effect",[1170,1243,1244],{},"The config was bundled into the exe",[1170,1246,1247],{},"Load it from beside the executable",[1152,1249,1250,1253,1256],{},[1170,1251,1252],{},"Works on your Windows, not theirs",[1170,1254,1255],{},"Missing Visual C++ runtime, or a newer OS build",[1170,1257,1258],{},"Build on the oldest Windows version you support",[1152,1260,1261,1264,1267],{},[1170,1262,1263],{},"Console window flashes and closes",[1170,1265,1266],{},"Double-clicked, finished, exited",[1170,1268,1269,1270,1273],{},"Keep ",[14,1271,1272],{},"console=True"," and tell users to run it from a command prompt, or pause on exit",[126,1275,1277],{"id":1276},"performance-and-scale-notes","Performance and scale notes",[10,1279,1280],{},"Packaging does not change how fast the report runs, but it changes what a fix costs. A bug in a bundled script is a rebuild, a virus-scan cycle and a redistribution to every desktop that has a copy — measured in days rather than minutes. Two habits keep that manageable: put everything that might plausibly change into the external config file, and print a version string at startup so a support conversation begins with which build the user is running.",[131,1282,1284],{"className":486,"code":1283,"language":488,"meta":136,"style":136},"__version__ = \"1.4.0\"\nlog.info(\"regional-report %s\", __version__)\n",[14,1285,1286,1296],{"__ignoreMap":136},[140,1287,1288,1291,1293],{"class":118,"line":142},[140,1289,1290],{"class":195},"__version__",[140,1292,905],{"class":505},[140,1294,1295],{"class":149}," \"1.4.0\"\n",[140,1297,1298,1301,1304,1307,1310,1312,1314],{"class":118,"line":277},[140,1299,1300],{"class":501},"log.info(",[140,1302,1303],{"class":149},"\"regional-report ",[140,1305,1306],{"class":273},"%s",[140,1308,1309],{"class":149},"\"",[140,1311,556],{"class":501},[140,1313,1290],{"class":195},[140,1315,631],{"class":501},[10,1317,1318],{},"If you find yourself rebuilding often, that is the signal to move the job onto a server and hand the user the output instead of the tool.",[126,1320,1322],{"id":1321},"conclusion","Conclusion",[10,1324,1325],{},"Package a report script only when the user genuinely cannot have Python. When you do, build in a clean environment, add the hidden imports pandas and openpyxl need, keep read-only assets inside the bundle and everything editable beside the executable, prefer a one-directory build for startup speed and fewer antivirus problems, and ship a folder with a config file and a three-line README. Then test it on a machine that has never seen Python, because that is the only test that matches how it will be used.",[126,1327,1329],{"id":1328},"frequently-asked-questions","Frequently asked questions",[10,1331,1332,1336],{},[1333,1334,1335],"strong",{},"Can I build a Windows .exe on Linux or macOS?","\nNo. PyInstaller freezes the interpreter and libraries of the machine it runs on, so a Windows executable must be built on Windows. Use a Windows CI runner or a virtual machine if your own machine is not.",[10,1338,1339,1342],{},[1333,1340,1341],{},"Why is the executable 60 MB when the script is 80 lines?","\npandas and NumPy carry compiled extensions and data files, and the bundle also contains a Python interpreter. Excluding matplotlib and the test packages usually saves 20-30 MB; dropping pandas in favour of plain openpyxl saves far more.",[10,1344,1345,1348],{},[1333,1346,1347],{},"My exe works but a colleague's antivirus quarantines it — what now?","\nUnsigned one-file executables that unpack themselves at startup are a common false positive. Build one-directory instead of one-file, and sign the binary if you can; both reduce the heuristics that trigger it.",[10,1350,1351,1354,1355,1358,1359,1361],{},[1333,1352,1353],{},"How do I keep the config file editable after packaging?","\nRead it from beside the executable rather than from inside the bundle. ",[14,1356,1357],{},"sys.executable","'s parent is that directory when the app is frozen, so ship ",[14,1360,104],{}," next to the exe and load it from there.",[126,1363,1365],{"id":1364},"related","Related",[10,1367,1368],{},"Up to the parent guide:",[1370,1371,1372],"ul",{},[1373,1374,1375,1377],"li",{},[22,1376,25],{"href":24}," — the alternatives to packaging, and when each is right.",[10,1379,1380],{},"Related guides:",[1370,1382,1383,1389,1395,1402],{},[1373,1384,1385,1388],{},[22,1386,1387],{"href":173},"Keep Excel Report Settings in a Config File"," — the file that must stay outside the bundle.",[1373,1390,1391,1394],{},[22,1392,1393],{"href":168},"Build a Command-Line Tool for Excel Reports with argparse"," — the interface the executable exposes.",[1373,1396,1397,1401],{},[22,1398,1400],{"href":1399},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-excel-template-with-python-openpyxl\u002F","Fill an Excel Template with Python and openpyxl"," — the template that travels inside the bundle.",[1373,1403,1404,1407],{},[22,1405,1406],{"href":1138},"Run a Python Excel Script on Windows Task Scheduler"," — scheduling the packaged executable on the user's own machine.",[1409,1410,1411],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":136,"searchDepth":277,"depth":277,"links":1413},[1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424],{"id":128,"depth":277,"text":129},{"id":178,"depth":277,"text":179},{"id":247,"depth":277,"text":248},{"id":697,"depth":277,"text":698},{"id":1048,"depth":277,"text":1049},{"id":1079,"depth":277,"text":1080},{"id":1143,"depth":277,"text":1144},{"id":1276,"depth":277,"text":1277},{"id":1321,"depth":277,"text":1322},{"id":1328,"depth":277,"text":1329},{"id":1364,"depth":277,"text":1365},"2026-08-10","Ship a report script to someone with no Python: a one-file build, the hidden imports pandas and openpyxl need, keeping the config and template outside the bundle, and how to find your own files at runtime.","md",[1429,1431,1433,1435],{"q":1335,"a":1430},"No. PyInstaller freezes the interpreter and libraries of the machine it runs on, so a Windows executable must be built on Windows. Use a Windows CI runner or a virtual machine if your own machine is not.",{"q":1341,"a":1432},"pandas and NumPy carry compiled extensions and data files, and the bundle also contains a Python interpreter. Excluding matplotlib and the test packages usually saves 20-30 MB; dropping pandas in favour of plain openpyxl saves far more.",{"q":1347,"a":1434},"Unsigned one-file executables that unpack themselves at startup are a common false positive. Build one-directory instead of one-file, and sign the binary if you can; both reduce the heuristics that trigger it.",{"q":1353,"a":1436},"Read it from beside the executable rather than from inside the bundle. sys.executable's parent is that directory when the app is frozen, so ship config.toml next to the exe and load it from there.",{"breadcrumb":1438},[1439,1441,1444,1445],{"name":1440,"item":796},"Home",{"name":1442,"item":1443},"Automating Reporting Workflows","\u002Fautomating-reporting-workflows\u002F",{"name":25,"item":24},{"name":5,"item":1446},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller\u002F","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller",{"title":1449,"description":1450},"Package a Python Excel Script as an EXE (PyInstaller)","Build a standalone executable from a Python Excel report script with PyInstaller — hidden imports for pandas and openpyxl, bundled templates, external config, and size and antivirus gotchas.","package-a-python-excel-script-as-an-exe-with-pyinstaller","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller\u002Findex","how-to","EN_suK0ElnDHO_lRIpNp35K40uJKbjRu0EPv2ooLxno",[1456,1459],{"title":1387,"path":1457,"stem":1458,"children":-1},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002Findex",{"title":1460,"path":1461,"stem":1462,"children":-1},"Test Excel Output with pytest","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002Findex",1786800028366]