WhatThePy obfuscates a single Python file using compression, authenticated encryption, literal-table protection, and a compact runtime loader. The result is compact, messy code that still runs identically but is harder to casually inspect.
Important: This is for fun and learning. It deters casual viewers but will not stop a skilled reverse engineer with live process access. See SECURITY.md for the tested threat model.
Platform: Windows + CPython 3.10–3.14. Build and run on the same CPython minor version.
| Your Code | After WhatThePy |
|---|---|
def greet(name):
message = f"Hello, {name}!"
print(message)
return message
if __name__ == "__main__":
greet("World") |
...
洪ΠОΖНСΔΘΜΖФ鷹=lambda:_СΖΙ虎ΣΓФЙТХВГ(_Θ宙鷹荒鷹日ЙΖΥХДΑ(_У荒ЛΡ荒黃ΣНПΘР豹,_ДΒПΘΞНΠΡ麟麟Γ玄,_РК虎Π宙玄ΕНКΛ龍Ж,_麒鳳ΞБΕГОП宙БΞ獅),_ΥДΓ日Ζ地УΙΤ麒熊麒)
def _日И月黃宇ОНΖ麟龍麒洪():return bytes(_洪ΠОΖНСΔΘΜΖФ鷹())
_다허يεمα地러ص서Т風=_日И月黃宇ОНΖ麟龍麒洪()
def _ذרгΚЛΤ조さ洪터βש(_바かたצדαΜח春غР秋,_צカישىこضФオמП寒):
_אנЮ天라ΘШנ火昃הן=bytes(_v^186 for _v in [210, 219, 201, 210, 214, 211, 216]).decode()
_木סкעل寒бえいΚ머Φ=bytes(_v^109 for _v in [30, 5, 12, 95, 88, 91]).decode()
_川거黃麒جウ宙머辰けиף=__import__(_אנЮ天라ΘШנ火昃הן);_ךえ黃لظ허אס昃타麒霜=getattr(_川거黃麒جウ宙머辰けиף,_木סкעل寒бえいΚ머Φ)
_너さИر雨חר雪러Х퍼風=bytearray(len(_צカישىこضФオמП寒));_玄СβחΓ辰ΡΧסתقケ=32
for _소ד河そえΑض冬ЫてДذ in range(0,len(_צカישىこضФオמП寒),_玄СβחΓ辰ΡΧסתقケ):
_رΘЙЙ日ТРЫЪב山Γ=(_소ד河そえΑض冬ЫてДذ//_玄СβחΓ辰ΡΧסתقケ).to_bytes(8,'little')
_لפへζひחيטΩたカΩ=_ךえ黃لظ허אס昃타麒霜(_바かたצדαΜח春غР秋+_رΘЙЙ日ТРЫЪב山Γ).digest()
_قて차あ차Э雪ث黃ضΗ雪=_צカישىこضФオמП寒[_소ד河そえΑض冬ЫてДذ:_소ד河そえΑض冬ЫてДذ+_玄СβחΓ辰ΡΧסתقケ]
for _Яו暑火ءФعجПΣいق,_כ카ΧΣ터γΗכدすלר in enumerate(_قて차あ차Э雪ث黃ضΗ雪):_너さИر雨חר雪러Х퍼風[_소ד河そえΑض冬ЫてДذ+_Яו暑火ءФعجПΣいق]=_כ카ΧΣ터γΗכدすלר^_لפへζひחيטΩたカΩ[_Яו暑火ءФعجПΣいق]
return bytes(_너さИر雨חר雪러Х퍼風)
def _ع雪כΨ麒麟лΨα盈霜暑(_머머天로ט盈صלΡ커ط저,_머חتתРΥפ타ΨУへت):
_كМへしう가電けキ荒בね=bytes(_v^35 for _v in [75, 66, 80, 75, 79, 74, 65]).decode()
_クア風ΣЛд宿火玄Эи저=bytes(_v^237 for _v in [158, 133, 140, 223, 216, 219]).decode()
_л더פ虎ほ電Кδ冬다Τو=__import__(_كМへしう가電けキ荒בね);_き퍼水暑노זאΗ사لىט=getattr(_л더פ虎ほ電Кδ冬다Τو,_クア風ΣЛд宿火玄Эи저)
return _き퍼水暑노זאΗ사لىט(_머חتתРΥפ타ΨУへت+_머머天로ט盈صלΡ커ط저).digest()
def _火دね카ζ列УبلてבЗ(_저ЛЭ河로ם오רעخΣ카,_נ黃تعえカعظさΜ川エ):
_شסすけγЦВεزΞΞ코=_저ЛЭ河로ם오רעخΣ카[:16];_고زつ玄г宇ЦγЮע霜す=_저ЛЭ河로ם오רעخΣ카[16:]
_פ코ג자طاい코Б秋ىכ=_ع雪כΨ麒麟лΨα盈霜暑(_شסすけγЦВεزΞΞ코,_נ黃تعえカعظさΜ川エ)
continues... |
git clone https://github.com/techlinn/WhatThePy.git
cd WhatThePy
pip install .For development:
pip install -e ".[dev]"Requirements: Python 3.10–3.14 on Windows. WhatThePy has no required runtime dependencies.
whatthepy [file.py] [flags]
# or: python -m whatthepy [file.py] [flags]Flags:
--output PATH- write the stub to a specific path--force- overwrite existing stub/payload files--quiet- suppress progress and summary output--version- print the installed version--strip-docs- strip module/class/function docstrings. Off by default so frameworks that read__doc__at runtime (FastAPI endpoint descriptions, click/Typer help, sphinx,help()) keep working.--self-contained- embed everything in a single.py(no.datsidecar). Default is split mode (stub +.dat).--level {low,high}-high(default) encrypts string literals into a runtime-decrypted table;lowskips that for a faster, weaker pass.
WhatThePy is single-file: it obfuscates one .py at a time and does not rewrite cross-file imports.
- Split mode (default) keeps the encrypted payload in a sidecar
.datfile. This is the fastest cold start and the recommended default. - Self-contained mode embeds the payload in the stub source. Startup is slower and the file is larger, but distribution is a single file.
highlevel restores literals once at load time and is stronger.lowlevel skips literal-table encryption for faster builds and lower overhead.
Works with PyInstaller (split mode):
pyinstaller --onefile --clean --add-data "yourfile_payload.dat;." yourfile_obfuscated.pyStatic imports are emitted under a non-executing PyInstaller scan guard. Add --hidden-import=NAME for modules loaded dynamically (importlib.import_module(...) / __import__(...) with non-literal names).
WhatThePy is designed so an obfuscated file runs identically and compiles with PyInstaller without runtime errors. Verified against f-strings, match/case literal patterns, PEP 695 type/generics, from __future__ import annotations + dataclass forward refs, TypedDict, Enum, __all__, bytes literals, decorator string args, typing.get_type_hints, default-string args, walrus, closures, __slots__, try/except messages, struct.pack, asyncio, multiprocessing (spawn), __init_subclass__, metaclasses, generators, and function-local imports.
Things to know:
- Single-file only. No batch/package mode and no cross-file import rewriting. Obfuscate each entry script individually; sibling modules are imported as-is.
- Relative imports work when the obfuscated file is imported as part of its package (the import machinery sets
__package__). Running an obfuscated package module directly as a top-level script will fail relative imports - same as plain Python. - Docstrings are kept by default (so FastAPI/click/sphinx/
help()work). Use--strip-docsto remove them. inspect.getsourcereturns the obfuscated stub source (not your original) rather than raising;inspect.getfilereturns the stub path. This is expected.- Co-located data files (assets next to your source) are not bundled automatically. With PyInstaller, add each via
--add-dataand read them fromsys._MEIPASSat runtime (fall back toos.path.dirname(__file__)when not frozen). - Python version pin. The stub checks
sys.version_infoand refuses to run on a different CPython minor version than the one it was built with. Build and run on the same Python interpreter. With PyInstaller this is automatic (the bundled interpreter matches).
| Threat | Protected? |
|---|---|
| Script kiddies | Yes |
| Casual copy-paste | Yes |
| Quick glances | Yes |
| Basic static analysis | Yes |
| Tampered payload files | Yes |
| Simple Python loader monkeypatches | Partially |
| Motivated attackers with debuggers | No |
| Live code-object dumping | No |
Good for: Learning, CTF challenges, deterring casual viewers Not for: Protecting secrets, valuable IP, or anything critical
See SECURITY.md for the exact tested and untested attacker capabilities.
Every build applies fail-closed payload integrity checks and per-build randomization, so no two builds share a fixed pattern. Corrupted payload or key data is rejected instead of running a fallback. No anti-debugging is used, so legitimate debug / coverage / pytest runs never trip.
The specifics - how the key is derived, what gets randomized, and how the loader validates a payload - are intentionally not documented here. An open-source obfuscator that explains its own defenses in the README hands a roadmap to everyone. Read whatthepy/ if you want to understand a build.
Pure Python cannot beat a skilled reverse engineer who dumps the live code object from a running process. The measures above raise the bar for casual and mid-level reverse engineers and defeat static, WhatThePy-specific automated unpackers, but they do not stop a generic dynamic dumper. This is a weekend project, not a security boundary.
This is a fun project for learning and experimentation. It makes code harder to casually read but doesn't provide serious protection. Use it for fun, learning, or light deterrence - not for anything critical.
Made this as a weekend project and please.. Don't use it for serious security needs!
MIT License - Do whatever you want with it, just don't blame me!
Made with questionable decisions and too much redbull😭