TL;DR
A little-known Python feature can disguise malicious code as completely innocuous. With just a 4-byte change to a module's .pyc cache, malicious bytecode runs while hiding behind the source that scanners inspect but Python never actually executes. The poisoned cache is undetectable by every antivirus on VirusTotal, and runs without throwing errors. Here are the ways to stay safe.
Background
Python is an interpreted language. Before your computer can run a .py file, the human-readable source is translated into bytecode, a lower-level instruction format designed for speed. To avoid recompiling every time the code runs, the compiled bytecode is stored in .pyc files inside a __pycache__/ folder next to the source, one .pyc per imported module. If no changes have been made since the last compilation, Python runs the cached bytecode directly and skips the source entirely.
Python decides whether the cache is still valid by checking a small header of the .pyc file. The header doesn't store the source code itself, it stores a fingerprint of the source at the moment the cache was created. On every import, Python compares that fingerprint to the current source and decides whether to trust the cache or rebuild it. Our researchers have discovered that a small 4-byte edit to the header will disable Python's ability to detect discrepancies between the source and bytecode, allowing malicious code to execute from your unscanned cache.
Invalidation modes
Python's .pyc header contains a field that tells the interpreter how to decide whether the cache is still valid. There are three possible values, and each has very different security properties. Python 3.7 introduced the two hash-based modes to support reproducible deployment builds. The goal was legitimate, but the third mode unintentionally created a vulnerability that hasn't been addressed.
- Timestamp mode (the default). The header stores the source file's last-modified time and size at compile time, and on every import Python checks whether either value has changed. Any action that rewrites timestamps like git clone invalidates the cache and recompiles the code.
- Checked hash mode. Python re-hashes the file on every import and compares. Timestamps are irrelevant, only content matters, so the cache survives git clone but dies the moment anyone edits the source, even to add a comment.
- Unchecked hash mode (the most dangerous). It uses the same hash-based header, but flips one bit in the flag field to tell Python not to bother checking. The hash is still there, Python just never reads it. The cache is trusted unconditionally, until something deletes it.
The stdlib exposes this through py_compile.compile(..., invalidation_mode=PycInvalidationMode.UNCHECKED_HASH), but that path leaves traces in build logs. Our researchers skipped the API entirely and patched the flag byte directly in an existing .pyc header.
Threat model
This technique matters not because the mechanism is exotic, but because it inverts a core assumption of every Python security process in use today.
Review bypass: Human code review, automated PR diffing, git blame, and most static analyzers all inspect the .py. The interpreter executes the .pyc. A poisoned cache makes the two diverge silently, and source review starts lying to you.
Supply chain: .pyc files routinely ride along in committed git repositories, PyPI wheels and sdists, Docker layers, ML model artifacts, CI/CD build outputs, and AI agent skill bundles, which are a particularly soft target since skills are small, trusted by default, and rarely audited with the same rigor as a PyPI package. A compromised build step that emits a poisoned .pyc alongside a legitimate .py is effectively invisible to downstream consumers who audit the source.
Static scanners don't see it: Marshalled bytecode doesn't look like code to signature-based tooling, no plaintext strings to match, no AST to parse, no familiar entropy profile. Our researchers uploaded two independent poisoned .pyc samples to VirusTotal and every one of the 70 antivirus engines missed them:
- 10f63c3e2ada149083488f4df74aead7df40079908c719c547d9baecaf10b71d
- fcc7f0fc3cd5e9d618befe697f5cd1caa42fa92f84dc606ed12b5c8a0d8e9b71

Silent Proxy: An unchecked-hash .pyc already ignores the source. But if the source and bytecode don't match, calling a function the source defines but the bytecode doesn't will raise an AttributeError. The victim will notice, but only after the malicious code has already executed. A proxy .pyc solves this by doing two things at import time:
- Execute the malicious payload.
- Read and execute the original .py source in the current module's namespace, so every function, class, and variable the reviewer sees in the source becomes a real attribute of the imported module.
The module behaves exactly as its source says, while attacker code has already run.
Proof of Concept
Below is the proof-of-concept builder. It takes an innocent target .py, compiles it, and rewrites the resulting .pyc header to unchecked-hash mode.
The reviewer sees a two-line helper. The runtime printed PWNED first, then returned the exact result the source promises. Both things happened. Only one is visible.
Inspecting the header:
Flags = 01 00 00 00 → unchecked hash. The hash field is meaningless but unread. Python loads the cache and runs the bytecode.
Staying safe
Treat .pyc files as executable code, not as disposable build artifacts, and don't let them travel with your source.
- Never commit __pycache__/ or .pyc files to git. Keep them in your top-level .gitignore.
- Distrust bytecode in unpacked wheels, tarballs, and skill bundles. If you're auditing a dependency, audit or strip its caches too.
- Disable cache writes in security-sensitive contexts. Run Python with PYTHONDONTWRITEBYTECODE=1 and -B to stop caches from being written at all.
- Force hash validation at runtime. Set --check-hash-based-pycs=always to make the interpreter validate every hash-based .pyc regardless of what its header claims.
- For AI agent and skill platforms, refuse bundles that contain __pycache__/. Compile skills at deploy time from source only.
- Quick integrity check: any .pyc file with byte 0x04 set to 0x01 is in unchecked-hash mode and should be treated as hostile until proven otherwise.
Treat every runtime artifact as executable.
Assess your AI supply chainWhat’s New from Alice
Poisoned Python Bytes & Malicious Caches
Python .pyc caches can execute malicious bytecode while clean source code passes review. Learn how poisoned caches bypass scanners, exploit supply-chain trust, and what teams can do to stay safe.
AI in Finance: From Money Laundering to Deepfakes
Dr. Janet Bastiman has been making convincing deepfakes since 2017, long before most people knew the word. Now the Chief Data Scientist at Napier AI, she joins Mo to get into why fraud is actually easier to catch than money laundering, how a deepfake already talked a finance team out of millions, and why the human analysts checking AI matter more than ever.
It Takes AI to Break AI: The Case for AI Red Teaming
As AI systems gain autonomy, organizations need security approaches built specifically for AI behavior. Learn why AI-driven red teaming is becoming a critical defense layer.
Demystifying AI Red Teaming
Your AI passed every check. That doesn't mean it's safe. Learn how to red team AI systems before adversaries find the gaps you missed.
