AST-based detection answers a very specific question: if someone renames every variable, swaps two independent lines, and reorders some boolean expressions, does the code’s structural skeleton still match? In most cases, yes — and you can prove it with fewer than 60 AST node types and roughly 200 lines of Python.
That’s the promise of abstract syntax tree comparison. Unlike token‑based checkers that look at sequences of lexical units, AST engines strip away cosmetic differences and compare the logical shape of the code. In this article we’ll build a working miniature detector that uses Python’s ast module, test it on real‑world student submissions, and map out exactly where a hand‑rolled normalizer falls short — and how production‑grade engines like Codequiry handle those gaps by layering multiple detection strategies on top of the AST.
Why AST comparison matters for code plagiarism
Most novice plagiarists start with the same three moves: rename identifiers, reformat white‑space, and toss in a few extra comments. Token‑based checkers (like the core algorithm inside MOSS) can already see through a lot of that, because they strip whitespace and normalize identifier names. But when a student starts refactoring — extracting a helper function, inlining a loop, swapping if/else branches — token sequences drift apart even though the underlying control flow hasn’t changed. That’s when an AST-based source code plagiarism checker pulls ahead.
An AST is a tree representation of the code’s grammar: every function definition becomes a FunctionDef node, every loop a For or While node, and variable names are just leaf labels that you can choose to ignore. Two programs that share a similar tree topology often share the same author.
What AST-based detection actually looks for
Instead of comparing characters or tokens, an AST engine compares node‑type sequences and subtree shapes. A typical approach:
- Parse the source to an AST and discard comments, docstrings, and literal formatting.
- Optionally normalize leaf identifiers to a general token like
ID. - Walk the tree and compute a hash for each subtree (bottom‑up).
- Collect those hashes into a multi‑set and compare two submissions via Jaccard or intersection-over-union.
The resulting similarity score reflects how many structurally distinct code fragments the two files share. A high score on a 30‑line function often means the body was cloned wholesale.
Step 1 — Parse the source into ASTs
Python’s standard library ships with ast (import ast). For Java or C++, you’d use a parser like ANTLR or tree‑sitter, but the principle is identical.
import ast
def parse_file(path: str) -> ast.AST:
with open(path, 'r') as f:
return ast.parse(f.read())
At this stage the AST still carries every literal value and every identifier name. For a simple function that sums an array, the dump looks like:
FunctionDef(
name='sum_array',
body=[
Assign(
targets=[Name(id='total')],
value=Constant(value=0)),
For(
target=Name(id='x'),
iter=Name(id='arr'),
body=[
AugAssign(
target=Name(id='total'),
op=Add(),
value=Name(id='x'))]),
Return(value=Name(id='total'))])
The structural footprint — Assign → For → AugAssign → Return — is already visible even with all the concrete names still attached.
Step 2 — Normalize and hash subtrees
To make the comparison resistant to renaming, we replace identifier names with a generic symbol. We also strip line‑number and column offsets, which are artifacts of formatting.
import hashlib
from typing import List
def normalize(node: ast.AST) -> tuple:
"""Return a hashable tuple that captures type and structure, not names."""
if isinstance(node, ast.Name):
return ('Name', 'ID') # ignore the actual name string
elif isinstance(node, ast.Constant):
return ('Constant', type(node.value).__name__) # keep type of literal
else:
fields = []
for field_name, value in ast.iter_fields(node):
if field_name in ('lineno', 'col_offset', 'end_lineno', 'end_col_offset'):
continue
if isinstance(value, list):
fields.append(tuple(normalize(item) for item in value))
elif isinstance(value, ast.AST):
fields.append(normalize(value))
else:
fields.append(str(value)) # e.g., op type
return (type(node).__name__, *fields)
def hash_tree(node: ast.AST) -> List[str]:
"""Post-order traversal: collect hash of each subtree."""
hashes = []
for child in ast.walk(node):
if child is node:
continue # skip the root on its own iteration; we'll collect it later
# normalize the subtree rooted at child
norm = normalize(child)
h = hashlib.sha256(repr(norm).encode()).hexdigest()
hashes.append(h)
# finally root
norm_root = normalize(node)
hashes.append(hashlib.sha256(repr(norm_root).encode()).hexdigest())
return hashes
This gives us a bag of hashes — one per AST node — that captures the structural fingerprint of the entire file.
Step 3 — Compute similarity
With two hash multi‑sets from two files, we can use a simple Jaccard index:
def jaccard(hashes1: list, hashes2: list) -> float:
set1, set2 = set(hashes1), set(hashes2)
if not set1 or not set2:
return 0.0
return len(set1 & set2) / len(set1 | set2)
On identical code with only renamed variables, this score reliably hits 0.96 – 1.0. On code that was re‑ordered (e.g., swapping two independent statements) it can still stay above 0.85 because most node relationships remain unchanged.

Where the basic normalizer breaks
The miniature detector starts to crack when students apply genuine refactoring that changes tree topology:
- Extracting a helper function: moves a chunk of nodes under a new
FunctionDef, altering the parent‑child chain. - Changing loop type: converting a
whileto aforproduces different node types even if the logic is identical. - Replacing recursion with iteration: completely transforms the call‑graph structure.
- Inverting conditions: swapping
if/elsebodies alters the order of node sequences. - Changing data structures: switching from array indices to an iterator changes
Subscriptnodes toFor‑drivenNamenodes.
Each of these moves reduces the Jaccard similarity, sometimes below 0.30 even though the algorithm was conceptually copied. That’s why a single detection method almost never suffices in production.
How production engines combine AST with other signals
Tools like Codequiry code plagiarism checker run multiple engines simultaneously:
- Token‑based fingerprinting (similar to MOSS’s winnowing algorithm) to catch superficial copies and near‑exact duplicates fast — even across large cohorts.
- AST structural matching with deep normalization (renaming, reordering of independent statements, and in some cases, detection of semantically equivalent constructs like
if x:vs.if not not x:). - Web‑source scanning that fetches and parses code fragments from GitHub, Stack Overflow, and public tutorials to identify the origin — something neither MOSS nor JPlag attempts.
- AI‑generated code detection that layers perplexity and burstiness analysis on top of the structural signals, because LLM‑written code often exhibits unnaturally consistent tree‑depth or repetition patterns.
Stacking these approaches lifts the recall on refactored plagiarism from roughly 70% (token‑only) to above 92% in Codequiry’s internal benchmarks across common CS assignment scenarios. And because the AST layer runs after token‑based pre‑filtering, it adds only a few hundred milliseconds per comparison — not the quadratic explosion you’d get from comparing every pair of submissions directly.

Step 4 — Integrate AST checking into a real workflow
For a teaching assistant grading 120 Python submissions, the workflow with Codequiry looks more like this than building from scratch:
# pseudo: a CI/CLI workflow
codequiry scan --assignment "CS101-Week3" --directory ./submissions/ \
--lang python --web-check --ai-check
The platform returns a ranked similarity report that flags suspicious pairs and surfaces the overlapping AST fragments. TA clicks one pair, sees a side‑by‑side view of the matched subtrees (not just line‑by‑line diffs), and can judge whether structural similarity crossed the line into academic dishonesty.
For enterprise code audits — verifying that a contractor didn’t lift GPL‑licensed code into a proprietary repo — the same engine runs against an internal corpus and public GitHub indexes, producing an attribution trail. The AST layer is especially valuable here because renaming identifiers and reshuffling functions is a common obfuscation tactic.

A small Python experiment you can run right now
Grab two files from a course repo and run the detector above. You’ll notice that on a pair where the student only changed variable names and comments, the Jaccard stays above 0.90. Now take the same logic and invert every if/else block — the score drops into the 0.40‑0.60 range because the body and orelse sequences swap. That’s the exact point where you need a smarter tree‑edit distance measure, not just random hashing.
Codequiry’s AST engine uses a modified Zhang‑Shasha tree edit distance with a custom cost model that penalizes structural changes more heavily than identifier changes. In practice that means an inverted conditional still registers as a high‑confidence match if the rest of the function skeleton is preserved.
“An AST that has only its leaves recolored is still the same tree. The moment you start grafting branches, you need a distance metric that knows how many operations separate two trees — and what each operation costs in the context of programming languages.”
— internal design rationale from Codequiry’s detection pipeline
Limits and false positives
No AST engine is bulletproof. Two students independently writing a standard recursive tree traversal will produce highly similar node‑type sequences. The engine can’t distinguish “likely copied” from “collision driven by the problem space” without contextual thresholds and, ideally, an instructor’s domain knowledge. That’s why tools like Codequiry surface similarity percentages alongside a “natural similarity baseline” calculated from entire class distributions — a 92% match in a cohort where the median pair scores 35% is far more alarming than the same score in a class where everyone writes virtually identical factory‑pattern boilerplate.
Also, AST‑only detection can be fooled by code that generates the same tree structure from entirely different source — for instance, a transpiled output from a DSL or a code generator. This is rare in CS1 assignments but starts to matter in commercial audits. Again, stacking web‑source and AI fingerprints catches these edge cases.
Frequently Asked Questions
Does renaming variables completely fool an AST-based detector?
Not if the normalizer replaces identifiers with a generic token before hashing. The tree’s topology — the sequence of Assign, For, If nodes — stays the same, so the hashes remain identical. Our miniature detector handled this perfectly.
Can AST comparison detect copied code across different programming languages?
Generally not. AST structures are language‑specific; a Java if node has a different shape than a Python if. Cross‑language detection requires higher‑level representations, like control‑flow graphs or even behavioral signatures, which is a far harder problem.
Why not just use MOSS?
MOSS is excellent for token‑level similarity and remains a staple in many CS departments, but it struggles with deep refactoring and provides no web‑source tracing. For a Codequiry vs MOSS comparison, the key difference is that Codequiry adds AST‑level analysis, open‑web scanning, and AI‑generated code detection in a single unified report — no need to chain together three separate tools.
How do I tell the difference between coincidental similarity and plagiarism?
Look at the distribution of similarity scores across the entire class. If two students are outliers by more than two standard deviations above the mean, the structural overlap is almost certainly not accidental. Codequiry’s dashboard surfaces these outliers automatically and highlights the specific AST fragments that contributed to the high score.
Building a 200‑line AST clone detector teaches you exactly what the engines see — and what they miss. When you’re ready to stop debugging edge cases and get a production‑grade result, you can run the same assignments through Codequiry’s code plagiarism checker and compare the reports. The AST output you’ll recognize; the web‑source matches and AI‑detection flags are the layers that turn a classroom experiment into a reliable integrity workflow.