AST-level obfuscation handles every Python language feature up to 3.11 syntax — f-strings, the walrus operator, positional-only params, dataclasses, decorators, async/await, PEP 585 type hints, X | Y unions, match/case, and except* — and the obfuscated output stays backward-compatible, running correctly all the way down to each feature's introduction version. The two current gaps: PEP 695 type parameters (def f[T], 3.12+) and t-strings (PEP 750, 3.14+) fail with a SyntaxError, because the engine parses with CPython 3.11.
"Will obfuscation break my code?" is the first question anyone selling Python software asks, and most answers online are hand-waving. So I tested it properly: one small fixture for each major Python language feature, obfuscated with the full pro pipeline, then executed on seven real CPython builds — 3.8.20, 3.9.25, 3.10.20, 3.11.5, 3.12.13, 3.13.14 and 3.14.2.
The headline is reassuring, with two specific exceptions worth knowing before you ship. Here's the full matrix and exactly how it was produced.
The matrix
Each feature has a runnable fixture that prints a known result. It's obfuscated on the 3.11 engine (all layers: rename, string/int encoding, control-flow flattening, junk injection, import/attr/builtin hiding), then both the original and the obfuscated output are run on every version. "Obfuscated output runs" means it executed and produced the correct result.
| Language feature | Introduced | Obfuscates? | Obfuscated output runs on |
|---|---|---|---|
| f-strings | 3.6 | Yes | 3.8 – 3.14 (all tested) |
Walrus operator := | 3.8 | Yes | 3.8 – 3.14 |
Positional-only params / | 3.8 | Yes | 3.8 – 3.14 |
| Dataclasses | 3.7 | Yes | 3.8 – 3.14 |
| Decorators | ≤3.8 | Yes | 3.8 – 3.14 |
| async / await | 3.5 | Yes | 3.8 – 3.14 |
PEP 585 hints list[int] | 3.9 | Yes | 3.9 – 3.14 |
Union types X | Y | 3.10 | Yes | 3.10 – 3.14 |
Pattern matching match/case | 3.10 | Yes | 3.10 – 3.14 |
Exception groups except* | 3.11 | Yes | 3.11 – 3.14 |
Type params def f[T] (PEP 695) | 3.12 | No — SyntaxError | — |
| t-strings (PEP 750) | 3.14 | No — SyntaxError | — |
The pattern is simple: everything the 3.11 engine can parse, it obfuscates — and the obfuscated output runs on every Python version where that feature exists, down to the version that introduced it. Obfuscation did not break backward compatibility for any supported feature.
The reassuring part: backward compatibility holds
The most common worry is that obfuscation will quietly bump your minimum Python version — that an obfuscated 3.8 script will suddenly need 3.10. It didn't. For every feature that exists on older Pythons, the obfuscated output ran correctly all the way back to that feature's introduction version. Obfuscated f-string code still runs on 3.8; obfuscated match/case code runs on 3.10+; the transformer's own injected wrapper and junk code stayed within the syntax the input already used.
For anyone selling software that has to support a range of Python versions, that's the important result: obfuscate against your real target and the output keeps the same compatibility floor. Async functions, dataclasses, decorators, and complex type hints all survived every transformation layer intact and produced identical results.
The two gaps: 3.12+ type parameters and t-strings
Two features failed to obfuscate at all, and it's the same root cause: the engine parses your code with CPython 3.11, so syntax that only exists in 3.12+ can't be parsed.
- PEP 695 type parameters —
def first[T](xs: list[T]) -> T:andtype Alias = ...(Python 3.12+) raise aSyntaxErrorin the obfuscator. - t-strings —
t"..."template strings (PEP 750, Python 3.14+) also raise aSyntaxError.
Note the precise scope: this is only about the new syntax itself. Ordinary code that *targets* 3.12 or 3.13 obfuscates and runs fine — every non-syntax-specific fixture passed on 3.12 and 3.13. You only hit the wall if a file actually uses PEP 695 type-parameter syntax or t-strings. The practical workaround today is to write those few declarations in the pre-3.12 equivalent (TypeVar instead of [T]), or keep the handful of files that need the new syntax unobfuscated and lean on obfuscating the rest.
Overhead: output size
Full pro-mode obfuscation is not small — control-flow flattening and junk injection add substantial bulk. On these tiny single-feature fixtures the expansion looked enormous (100–450×) simply because the fixtures are a few dozen bytes and the injected scaffolding dominates. On real modules the ratio is far lower: in a separate retained test, a 718-byte fixture became a 30,497-byte standalone file — about 42× — and the ratio keeps shrinking as your actual code grows relative to the fixed scaffolding.
If output size matters (shipping to a size-constrained target), turn off flattening and junk injection, or run the result through minification — you trade some resistance for a much smaller file. Renaming and string encryption alone add very little size.
How this was tested (reproduce it)
Environment: the obfuscator engine on CPython 3.11.5; obfuscated output executed on 3.8.20, 3.9.25, 3.10.20, 3.11.5, 3.12.13, 3.13.14, 3.14.2 (Windows x86-64). Full pro pipeline enabled. Each fixture prints a fixed marker on success, and a run counts as passing only if that marker appears in stdout. Here are two of the fixtures so you can reproduce the method:
def cls(c):match c:case [a, b]:return a + bcase _:return -1print("RESULT_OK" if cls([2, 3]) == 5 else "bad")
def first[T](xs: list[T]) -> T:return xs[0]print("RESULT_OK" if first([9, 8]) == 9 else "bad")
Obfuscate each with the Python Obfuscator, then run the output on each interpreter with the marker check. If you're selling software, this is exactly the pre-ship test worth automating — see how to protect Python before selling it.
Obfuscate your Python and test it
Run your module through the free obfuscator, download standalone Python, and check it against every version you ship for.
Open the Python ObfuscatorFree tools mentioned here
Related guides
Frequently asked questions
Does obfuscation break async, dataclasses, or decorators?
No. In testing across Python 3.8–3.14, async/await, dataclasses and decorators all obfuscated cleanly and the obfuscated output produced identical results on every version that supports them. These common features survive all the transformation layers.
Will obfuscating raise my minimum Python version?
No — backward compatibility held for every supported feature. Obfuscated code that used only 3.8 features still ran on 3.8; the transformer's injected code stayed within the syntax your input already used. Obfuscate against your real target version and the output keeps the same compatibility floor.
Can it obfuscate Python 3.12 or 3.13 code?
Yes, as long as you don't use PEP 695 type-parameter syntax (def f[T], type X = ...). Ordinary code targeting 3.12/3.13 obfuscates and runs fine — every non-syntax-specific fixture passed on both. Only the new 3.12+ syntax itself is unsupported today.
Why do type parameters and t-strings fail?
Because the engine parses your source with CPython 3.11, and both PEP 695 type parameters (3.12+) and t-strings (3.14+) are syntax that 3.11 cannot parse — so they raise a SyntaxError before any obfuscation happens. Rewrite those few declarations in the pre-3.12 form, or keep the files that need them unobfuscated.
How much bigger is obfuscated output?
With the full pro pipeline (flattening + junk), a real 718-byte module became ~30 KB (~42×) in a retained test, and the ratio drops as your code grows. Tiny snippets look far worse (100×+) only because the fixed scaffolding dominates. Disable flattening/junk or minify the result if size matters.