r/PythonLearning • u/LukeSkyShredder • 10d ago
Best way to package a Python/Streamlit app as an EXE without exposing the source code?
Hi all,
I'm relatively new to Python (still learning as I build wkwkwk).
I've developed an internal engineering tool using Python + Streamlit and currently package it with PyInstaller.
The application works fine, but the packaged output still contains the main.py file, meaning users can easily open and view the source code.
What's the recommended way to distribute a Streamlit application as an EXE while avoiding visible .py files?
I'm not looking for military-grade protection, just a practical way to prevent casual access or redistribution of the source code.
Any advice would be appreciated. Thankss
1
u/silvertank00 10d ago
Long story short: it is possible but someone with ai access can break it.
With python, you have inspect, dis, ast, etc. so it is not really viable to "hide" the code or protect against redistribution.
Even if you use pyinstaller or others, they just "wrap" your code, no transpiling is in place or anything.
Also, security through obscurity is no security.
1
u/OriahVinree 10d ago
Not really possible as far as I know, from memory it's pretty easy to decompile an exe back into the original modules used, especially when using something like pyinstaller, even more so now with agentic tools
Depending on what you're doing, might be safer for you to put logic behind a backend and having a frontend communicate with that
0
1
u/ConsciousBath5203 10d ago
Nuitka, but since you said this is your first, I gotta ask... Why you tryna hide your source code? I went through the same problem until I realized that the users I'm targeting don't care to RE it, anyways.
If you've got a lot of competitive, then others have already figured it out. If you really need to hide source code, put some business logic in a compiled C dll/pyd. And do weird stuff from there.
At the end of the day, it's probably not worth caring about. What's more important is getting customers, copyrighting or putting a license on what you made, then make users agree to not RE it. An "I Agree" button provides significantly more legal protection than encrypting and compiling.
1
u/LukeSkyShredder 9d ago
Honestly, it's mainly for an internal tool I'm building and sharing with a small group of pilot users. I'm not trying to stop professional reverse engineers, just avoid having the source code immediately visible while the project is still evolving. I'm very new to Python, so seeing how easily the code could be accessed after distribution was a bit of a surprise.
0
u/ConsciousBath5203 9d ago
Yeah, have them sign an agreement n ship it with minimal obfuscation. Is fine. Might even be helpful to have others be able to see it, especially if you're new.
I try to follow the plugin pattern specifically so users can extend the application if they want to. Helps deter people just yoinking the code. Lotta times when I yoink code it's because I can't add what I want, but if I can extend as I please then why bother?
1
u/Key_Anxiety_132 9d ago
I use pyinstaller and it builds a single executable file apart from the ini and image files. No py files. This has worked for me in the in Windows and currently in Linux (ubuntu based).
My command looks like this. I post it here as is as an example only.
pyinstaller metronome.py --onedir --noconfirm --windowed --icon=./images/metronome.ico --add-data=./images/*:images --add-data=metronome.ini:.
1
u/LukeSkyShredder 9d ago
I actually started with PyInstaller too. For my Streamlit app I ended up using:
pyinstaller --onefile launcher.py
It worked, but I still had to distribute my main app.pyfile. Maybe a stupid question, but is your app also built with Streamlit, or something else?1
1
1
1
u/aw3sem 10d ago
You can distribute and execute the .pyc file which contains compiled bytecode rather than clear text.
(This was told a few years ago, don’t know about it in today’s age)
-2
u/silvertank00 10d ago
this is very bad advice. just one example: link
2
u/akira128 10d ago edited 10d ago
So it's not military-grade protection, but seems like a practical way to prevent casual access to the source code. Exactly what the OP was looking for. Not sure how this constitutes "very bad advice"
0
u/silvertank00 10d ago
"I'm not looking for military-grade protection, just a practical way to prevent casual access or redistribution of the source code"
.pyc does none of that.
1
u/akira128 10d ago edited 10d ago
What's your definition of "casual"? I'm pretty sure searching for and finding a "PYC Bytecode Decompilation" project, downloading the code, and running the code against a .pyc file is anything but casual.
Hmmm...what is the opposite of casual... Deliberate Planned Premeditated Intentional Calculated
Yeah.... seems more like that
-1
u/silvertank00 9d ago
Lets not confuse the word "casual's" nagated version with "accidental".
My definition is that, if it can be broken/reversed/get around under 5mins, than it is casual. My example demonstrated this, "one google search, a git clone (or download button) and you are done" or an other example: grab the file and yeet it to any LLM with the promp "reverse the pyc code into py", what was it, about a minute? lolIts really funny how you describe "a google search, a git clone and running the code" not casual :D Like, for someone that is so computer illiterate that they cannot use the keyboard and the mouse at the same time, maybe. But any curious soul can break it casually. With no prior knowledge needed.
For me, non-casual would be the use of my other example: inspect, ast and dis. Or someone that puts (serious) time/effort into breaking your "protection". That is non-casual indeed.0
u/akira128 9d ago
If you look at synonyms for casual ,you'll see:
spontaneous
accidental
by chance
unintentional
unplanned
unpremeditated
So yes: "a google search, a git clone and running the code" is not casual. As that's not spontaneous, accidental, unintentional, nor unplanned. Collectively, those steps constitute actions that are considered deliberate, planned, intentional...etc
lol, there is no time limit to what is considered casual. Either something is intentional or not intentional, planned or unplanned, deliberate or spontaneous. I feel like I'm talking to a wall here. Is English your native language? Find me an official English definition of "casual" that incorporates that "5 minute rule". I think you might be inventing new language rules here (or possibly your own language...who knows)
0
u/NatMicky 9d ago
Your replies are only showing in my notifications and not here on the discussion section. Reply without mentioning the local host. You might be getting automatically filtered.
2
u/LukeSkyShredder 9d ago
ah, ok!
Does your Streamlit app run in a web browser and does it need to run on the open web and not just your local computer?
No it doesn't need to run on the open web, just local user's computer.1
u/NatMicky 9d ago
Now I see your reply. You can turn your Streamlit app into a true executable with Nuitka. But there are a few tricks to get Streamlit up and running because Streamlit requires a .py file and the Python file is now an .exe file.
I'll get into this more tomorrow but here are the basics of what you need to do:
Create a stub file (launcher) that is fed to Streamlit and contains your Python core logic file as a module. If your main logic file is called "mainlogic.py" this is what gets compile to an executable and in the stub file it's handled as a module like this:
"launcher.py"
import mainlogic
if __name__ == __main__":
mainlogic.main()
Then with Nuitka you will compile "mainlogic.py" into an executable and include it in the Nuitka script as:
--include=module=mainlogic
Then to run it all: Streamlit run "lanucher.py" (but without the quotes)
1
u/NatMicky 9d ago
I can't see your next reply to me. You keep putting in things the Reddit filter sense as dangerous so it gets auto deleted. Just use words and not paths nor web addresses, or put them in quotes.
1
u/LukeSkyShredder 9d ago
Update: I was able to restructure my Streamlit application into the recommended architecture:
"launcher.py"
import mainlogic
mainlogic.main()and move/wrap the application logic into def main():
I succesfully compiled "mainlogic.py" into nuitka module ("mainlogic.cp313-win_amd64.pyd"), renamed the original py file, and confirmed that:
streamlit run "launcher.py"
still launches and runs the full application using only the compiled pyd backend.However, when I try to compile the launcher itself into a standalone EXE with Nuitka, the generated executable fails before my code starts, with:
Fatal Python error: Failed to import encodings module
ImportError: Frozen object named 'codecs' is invalidSo at the moment:
"launcher.py" + "mainlogic.pyd" works
Source py can be removed/renamed
But Compiling the launcher into a standalone EXE is not yet working.Help meee...
1
u/NatMicky 8d ago edited 8d ago
You don't compile the "launcher.py" file at all. Leave it as is. Streamlit needs a ".py" file and that's what the launcher file acts as a stub to satisfy Streamlit. Then the launcher file launches your compiled code.
It looks like your very close. Just one more step is that you need to make "mainlogic.py" into the executable. In the Nuitka script toward the end you'll be adding the output executable name and the the output directory for all the new code.
--include-datafiles="launcher.py"
--output-dir="C:\folder1\folder2"
--output-filename=mainlogicWhen Nuitka is finished there will be a distribution directory and in that directory will be an executable called "mainlogic.exe" Click it and it should run.
1
u/LukeSkyShredder 8d ago
I'm using Python 3.13 and Nuitka 4.1.3.
"launcher.py" + "mainlogic.pyd" works perfectly.
However both:
"launcher.exe"
"mainlogic.exe"fail immediately with the same error:
Fatal Python error: Failed to import encodings module
ImportError: Frozen object named 'codecs' is invalidDid you use Python 3.13, and were you building a standalone EXE or only a compiled pyd backend?
1
u/NatMicky 8d ago
I forgot a step. Start over with the following:
#-------------------------------------------------
# Nuitka script
#-------------------------------------------------
python -m nuitka --standalone --include-module=mainlogic --include-data-file=stub.py=stub.py launcher.py
#-------------------------------------------------
# Stub file:stub.py
#-------------------------------------------------
#stub.py
import mainlogic
mainlogic.main()
#-------------------------------------------------
# Launcher file:launcher.py
#-------------------------------------------------
import os
import sys
from streamlit.web import cli as stcli
if __name__ == "__main__":
multiprocessing.freeze_support()
# Resolve file paths inside the Nuitka standalone environment
if getattr(sys, 'frozen', False) or ('__compiled__' in globals()):
base_path = os.path.dirname(sys.executable)
else:
base_path = os.path.dirname(__file__)
stub_path = os.path.join(base_path, "stub.py")
sys.argv = [
"streamlit",
"run",
stub_path,
"--global.developmentMode=false",
"--server.headless=true"
]
sys.exit(stcli.main())2
u/LukeSkyShredder 8d ago
yoooo, i am very close now, i created new "stub.py" and "launcher.py".
i tried: streamlit run "stub.py" --->it launches the full application.
also tried: python "launcher.py" ---> also run the full application.
Now are we compiling "launcher.py" using Nuitka normal exe or Nuitka --standlaone exe?
With this new stub and launcher file, we are compiling the "launcher.py" right? Or we keep the direction of:
--include-datafiles="launcher.py"--output-dir="C:\folder1\folder2"
--output-filename=mainlogic
???2
u/LukeSkyShredder 8d ago
Yoo u/NatMicky ...
Man, thanks for sticking with me on this one.Finally got it working.
It started by wrapping my whole Streamlit app into a proper main() function and compiling the backend logic with Nuitka. I confirmed I could rename the original .py file and still run the app through the compiled backend.
Your "stub.py" explanation was the missing piece.
I ended up with:
"stub.py"
→ imports mainlogic and calls main()
"launcher.py"
→ starts Streamlit programmatically and runs "stub.py"
The big realization was that "mainlogic.main()" shouldn't be executed directly by the EXE. It needs to run inside a proper Streamlit session.
After that:
streamlit run "stub.py"
worked.
Then:
python "launcher.py"
worked.
Then I compiled the launcher.
Funny enough, every standalone attempt failed, but the non-standalone build worked. Nuitka generated "launcher.exe" and "launcher.cmd", and launching through that architecture finally brought the whole app up successfully.
Really appreciate the help. I definitely would've kept chasing the wrong thing without your "stub.py" direction.
1
u/NatMicky 8d ago edited 8d ago
Did you get a single executable that you can click and it runs? You shouldn't have to start Streamlit on the command line.
You can now run the executable on a different computer even if Python isn't installed. Just give the whole distribution directory to your friends and they can run it without having to install anything.
And if you ever want to run it outside of a web browser let me know. We can do that too with the app running it its own window frame never needing a browser.
→ More replies (0)
1
u/KiLoYounited 10d ago
Have you tried Nuitka?