Skip to main content
Obfuscation

Python Obfuscation Compatibility Matrix: 3.8–3.14, Tested

By Mithun··9 min read
Quick Answer

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 featureIntroducedObfuscates?Obfuscated output runs on
f-strings3.6Yes3.8 – 3.14 (all tested)
Walrus operator :=3.8Yes3.8 – 3.14
Positional-only params /3.8Yes3.8 – 3.14
Dataclasses3.7Yes3.8 – 3.14
Decorators≤3.8Yes3.8 – 3.14
async / await3.5Yes3.8 – 3.14
PEP 585 hints list[int]3.9Yes3.9 – 3.14
Union types X | Y3.10Yes3.10 – 3.14
Pattern matching match/case3.10Yes3.10 – 3.14
Exception groups except*3.11Yes3.11 – 3.14
Type params def f[T] (PEP 695)3.12No — SyntaxError
t-strings (PEP 750)3.14No — 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 parametersdef first[T](xs: list[T]) -> T: and type Alias = ... (Python 3.12+) raise a SyntaxError in the obfuscator.
  • t-stringst"..." template strings (PEP 750, Python 3.14+) also raise a SyntaxError.

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 + b
case _:
return -1
print("RESULT_OK" if cls([2, 3]) == 5 else "bad")
pattern-matching fixture (runs on 3.10+)
def first[T](xs: list[T]) -> T:
return xs[0]
print("RESULT_OK" if first([9, 8]) == 9 else "bad")
the type-params fixture that the 3.11 engine rejects

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 Obfuscator

Free 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.

Keep reading