Tuesday, 1 September 2026

python bytecode python pyc vs py

 pyc advantage is faster on load, because its compiled, but it still requires a run time python interpretor, the main thing is others cant see ur source code directly and its a bit more diffcult to reverse engineer



server.py  →  server.pyc

A .pyc is the same Python interpreter executing a pre-parsed intermediate representation (bytecode for a stack VM), just skipping the compile-from-source step at import time. Nothing becomes an ELF binary:


server.py server.pyc Go binary (/edge)

What runs it python interpreter python interpreter (same) CPU directly

Form source text frozen bytecode + marshalled constants machine code

Readable as source? trivially no — only via decompiler, lossy no

This project dev only what the image ships yes

The venv's python process still interprets every line — exactly as before. The only mechanical differences: import finds server.pyc instead of server.py (both are equally importable), and startup is marginally faster (no parse/compile step). Zero behavior change.


The ladder, made concrete (since this was the source-visibility decision)


server.py     readable source            ← what a normal Python deploy ships

server.pyc    bytecode, decompilable     ← where we are (Dockerfile: compileall -b + delete .py)

Nuitka        transpiles to C → real ELF binary   ← the actual "make it a binary" option

Go rewrite    ELF binary                 ← the only *true* hiding, full rewrite of the app

.pyc protection is against casual reading — docker run ... cat /app/server.py fails because the file doesn't exist; strings server.pyc shows fragmented constants, not logic. A determined person with a decompiler (pycdc/uncompyle-style tools) can reconstruct most of it, though imperfectly — f-strings, comprehensions and some typing info come out mangled. That's the exact threat model you picked: "production shouldn't have readable Python," not "survive a hostile reverse-engineer."


If that bar ever rises, the escalation path (from the todo plan) is Nuitka — but that's a real bet: build complexity, fastmcp's decorator/introspection magic needs validation, and every dependency bump re-runs the compatibility test. The Go rewrite is the only total hiding, and we already chose it for the edge while keeping the app in Python deliberately.


One sharp edge worth knowing

.pyc files embed a magic number tied to the CPython minor version — server.pyc from 3.12 will refuse to run on 3.11/3.13. That's why the Dockerfile builds the pyc with the same image (python:3.12-alpine) that runs it, making the pairing structural rather than coincidental. It's also why you can't "just copy the venv" between hosts — irrelevant inside the immutable image, but remember it if you ever extract the venv for debugging on the prod host.


Bottom line: Dockerfile bytecode-only = source removed, bytecode kept — the interpreter still runs Python, just from a non-readable file. It's obfuscation-by-deletion: cheap, standard, fully reversible by you (git has the source), and sufficient for "the devops team can run it but shouldn't read it."




Exactly right on all three points — that's a precise summary. Expanding each slightly so the model is complete:


1. Interpreted at runtime — yes, same interpreter, same VM. The CPython execution model is two-stage: compile source → bytecode, then interpret bytecode on a stack machine. A .pyc just pre-computes stage one, shipping the frozen output of the compiler. At runtime, python server.pyc and python server.py execute the identical instruction stream — your decorators, async coroutines, the FastMCP tool dispatch — all still go through the interpreter's eval loop, instruction by instruction, just as before.


2. Slightly faster — but only at one specific moment: import time. The speedup is skipping lexing/parsing/codegen when the module loads. For this app that's milliseconds, once per process start. After import, zero runtime difference — same bytecode buffer, same eval loop, same speed. So it's a free side benefit, but if anyone asks "did we do this for performance?", the honest answer is no — it was for source-hiding; the perf delta is a rounding error.


3. Not readable without a decompiler — with a precise boundary on what "not readable" means:


Effort What they get

cat /app/server.pyc, strings fragmentary constants (strings, names), no logic — enough to spot a token or a URL, not to follow code

pip-installed decompiler (pycdc etc.) approximate source back, with real damage: f-strings, comprehensions, lambdas, and stripped type info come out garbled or missing

Determined reverse-engineering most business logic reconstructed

Also worth noting what doesn't hide at all: docstrings, comments... actually — docstrings survive into the .pyc verbatim (they're runtime objects), while comments do not exist in bytecode and vanish completely. So the bytecode hides your code but still leaks your prose (including anything sensitive accidentally left in a docstring, like the token example we saw in .env.example history). If you ever want prose scrubbed too, -OO at compile time strips docstrings/asserts:



python -m compileall -b -O2 ...

but you'd lose the helpful docstrings in tracebacks/debugging — not recommended for an app you still have to operate.


So the one-line mental model you've now got: .pyc = compiled for the interpreter, not to the machine — the hiding comes from the deleted source, the speed comes from the skipped compile, and the interpreter never stopped being the one doing the work. And in this image, all of Python — interpreter, stdlib, deps — is itself pyc-only too (find .venv -name '*.py' -delete), so the whole shipped surface is consistently bytecode, not just server.py.





FROM jfrog.corp.fortinet.com/docker-group/python:3.12-alpine AS py-builder

WORKDIR /app

COPY app/pyproject.toml app/uv.lock ./

RUN pip install --no-cache-dir uv \

 && uv sync --frozen --no-dev --no-install-project

COPY app/server.py ./

# Bytecode-only: compile server.py (and the whole venv) to .pyc next to the

# source, then delete every .py. Python imports .pyc seamlessly, so

# `uvicorn server:app` works unchanged; the app's source never ships.

RUN python -m compileall -b server.py && rm server.py \

 && python -m compileall -b -q .venv \

 && find .venv -name '*.py' -delete

No comments:

Post a Comment