Control-flow flattening rewrites a function's if/else and loops into a single while True loop driven by a numeric state variable that jumps between numbered blocks. The logic is unchanged — same inputs, same output — but the original top-to-bottom structure is gone, so you can't read the flow at a glance. Our obfuscator turned a 6-line function into a scrambled 7-state dispatcher that still returns ('positive', 10).
Renaming hides *what things are called*; control-flow flattening hides *what the code does next*. It's one of the stronger structural obfuscation techniques because it survives decompilation — a decompiler faithfully rebuilds the flattened mess, not your original branches. This is exactly what the Flatten layer in our Python Obfuscator does, so I ran it on a tiny function and captured the real output.
Every listing below is genuine output from that engine, and I verified the flattened version still runs and returns the same result.
What control-flow flattening is
Normal code reads top-to-bottom: an if here, a for there, a return at the end. Flattening throws that away. It chops the function into basic blocks, drops them all into one big while True loop, and adds a state variable (a dispatcher) that decides which block runs next. Each block ends by setting the state to the *next* block and looping back. The blocks are emitted in a scrambled order, so the numbers you read top-to-bottom have nothing to do with execution order.
Here's the function I fed in — deliberately simple so the transform is easy to see:
def classify(n):if n < 0:label = "negative"else:label = "positive"total = 0for i in range(n):total += ireturn label, total
The real flattened output
Running it through the obfuscator with only the Flatten layer on (194 → 978 characters), the branches and loop become a state machine:
def classify(n):llIIlllIllllIllll = 1810111while True:if llIIlllIllllIllll == 2215279:total = 0llIIlllIllllIllll = 9990608continueif llIIlllIllllIllll == 2579240:return (label, total)if llIIlllIllllIllll == 9990608:for i in range(n):total += illIIlllIllllIllll = 2579240continueif llIIlllIllllIllll == 1810111:if n < 0:label = 'negative'else:label = 'positive'llIIlllIllllIllll = 2215279continueprint(classify(5))
Read it and try to trace the flow: execution starts at state 1810111 (the if), jumps to 2215279 (total = 0), then 9990608 (the loop), then 2579240 (the return). The blocks are printed in a different order than they run — that's the whole point. I verified it still prints ('positive', 10).
Why it resists analysis (and decompilers)
String encryption can be undone by running the decoder; renaming just costs you readable names. Flattening is different: there is no original structure to recover. A decompiler turns the .pyc back into source, but the source it produces *is* the dispatcher — the if/for shape is genuinely gone from the compiled form. To understand the function, an analyst has to mentally execute the state machine, tracking the state variable by hand. On real code with dozens of states and nested loops, that's slow and error-prone.
Flattening obscures logic, not data. String literals and constants are still sitting in the code ('positive' above). Never rely on flattening alone to hide a secret — layer string encryption on top (or keep secrets off the client entirely). See how to protect API keys in Python.
Flattening is one layer, not the whole job
On its own, flattened code still has readable names and plaintext strings. The point of a real obfuscator is to stack layers so each one removes a different clue: rename identifiers, encrypt strings, flatten control flow, inject junk. Stacked, they multiply an attacker's effort. Our obfuscator applies flattening as one togglable layer among several — see the full picture in How to obfuscate Python code.
Two honest caveats: flattening grows your code (here, 5×) and adds a small runtime cost from the dispatcher loop, and a determined reverse-engineer with the right tooling can partially un-flatten it. It raises the bar; it isn't a wall.
Flatten your Python control flow — free
Run your code through our Python Obfuscator with the Flatten layer on: it rewrites branches and loops into an unreadable state machine, keeping the exact behavior. In your browser.
Open the Python ObfuscatorFree tools mentioned here
Related guides
Frequently asked questions
What is control-flow flattening in Python?
It's an obfuscation technique that rewrites a function's if/else branches and loops into a single while-loop 'state machine': a numeric state variable decides which block runs next, and the blocks are emitted in a scrambled order. The code behaves identically but its structure is unreadable.
Does control-flow flattening survive decompilation?
Yes — unlike string encryption, there's no original structure to recover. A decompiler rebuilds source from the .pyc, but that source is the flattened dispatcher, because the original branch shape is gone from the compiled form. The analyst has to trace the state machine by hand.
Does flattening hurt performance?
A little. The dispatcher adds a while-loop and repeated state comparisons, and the code grows (about 5× in my small example). For most scripts the overhead is negligible, but it's real, so flatten the sensitive functions rather than an entire hot-path codebase.
Can I flatten Python control flow online?
Yes. Our free Python Obfuscator applies control-flow flattening as a togglable layer (alongside renaming, string encryption and junk injection). Paste your code, enable the options, and it returns equivalent, runnable Python with the flow flattened — the exact transform shown in this article.