AI Text Watermarking in Python, Three Families Tested
Hands-on AI text watermarking in Python: how the three families work, what survives copy-paste, edits, and paraphrasing, and when to watermark or detect.

In this article
- 1.Where AI text watermarking fits in provenance
- 2.How green-list statistical watermarks work
- 3.Post-hoc edit watermarks and zero-width payloads
- 4.Synonym and syntactic rewrites
- 5.Zero-width character watermarks in Python
- 6.Semantic invariant watermarks that survive paraphrasing
- 7.A runnable robustness harness in Python
- 8.What the survival tests actually show
- 9.Production watermarking with SynthID and Claude
- 10.When to watermark, detect, or sign metadata
Every watermark you can build lives or dies on one question: which stage of the text pipeline do you control? Control generation, and a keyed statistical mark embedded in token choices beats any detector you could train on your output, because verification is a hypothesis test against a secret rather than a guess about writing style. Control only the finished text, and you can still smuggle bits through edits, but you are buying copy-paste robustness at best. Control neither, and you are down to post-hoc detection or signed metadata. Most arguments about AI text watermarking collapse onto that single axis.
Stay in the loop.
Get the latest posts and exclusive content delivered to your inbox.
Join 3 readers. No spam. Unsubscribe in one click, anytime.
This guide runs the three technique families as short Python programs: green-list statistical watermarks, post-hoc edit watermarks, and semantic invariant marks designed to survive paraphrasing. Then it attacks them. A reproducible harness applies copy-paste, word-level edits, and paraphrase, and reports the results as an explicit survival matrix. You also get the production context tutorials skip: what Google's SynthID-Text did across live Gemini traffic, what Anthropic disclosed about Claude, and a decision table for watermark versus detector versus signed metadata. The attack that separates these families is paraphrasing, not copying.
Where AI text watermarking fits in provenance
A watermark is a keyed signal embedded in text during or after generation and verified with a statistical test that requires the key. That definition does three jobs at once. It separates watermarking from post-hoc AI detection, a keyless classifier that guesses whether text is machine-generated from style alone and carries no binding to any particular system. It separates watermarking from C2PA content credentials, which sign an asset's origin and edit history into a cryptographic manifest that travels with the file, and that stops traveling once content is copied out of the container. And it matches the taxonomy current research uses for LLM watermarking, splitting schemes by whether they act at generation, at inference, or after the fact (a 2025 watermarking survey).
For a builder, the taxonomy matters less than the control question. Each family below assumes you can touch a different stage: the sampler, a rewrite pass, or the semantic content itself. Hold two questions open as you read. Which family survives which attack, and is watermarking or detection the right investment for your pipeline? The survival matrix and the closing decision table answer both.
How green-list statistical watermarks work

The scheme that started the modern wave is the green-list watermark of Kirchenbauer et al., 2023. At every generation step, hash the secret key together with the recent token context to seed a pseudorandom split of the vocabulary into a green list and a red list. Add a small positive bias to every green logit before sampling. The model barely changes, but the sampled text now carries an inflated fraction of green tokens, and only a key holder can tell green from red.
Detection is a one-sample z-test. Re-derive the split from the key, count green tokens, and compare against the expected rate. The verifier needs only the key and the suspect text: no model weights, no logit access, and it works on excerpts.
import hashlib, math
def is_green(key, prev, tok, gamma=0.5):
h = hashlib.sha256(f"{key}|{prev}|{tok}".encode()).digest()
return int.from_bytes(h[:8], "big") / 2**64 < gamma
def bias_logits(logits, key, prev, delta=2.0, gamma=0.5):
return {t: lp + (delta if is_green(key, prev, t, gamma) else 0.0)
for t, lp in logits.items()}
def detect(tokens, key, gamma=0.5):
hits = sum(is_green(key, tokens[i-1] if i else "<s>", tokens[i], gamma)
for i in range(len(tokens)))
n = len(tokens)
return (hits - gamma * n) / math.sqrt(n * gamma * (1 - gamma))
A |z| of roughly 4 to 6 across a few hundred tokens is the usual flag threshold. The knobs trade directly against each other. Gamma controls how much of the vocabulary is green, delta controls the bias strength, and both detectability and quality move with them. Crank delta and detection needs fewer tokens, but output quality can sag because sampling is steered by hash values rather than model preferences. The Aaronson line of work removes even that bias by encoding the signal in the sampling key itself, and the Christ-Gunn-Zamir formulation proves such marks undetectable, which is a big reason industrial systems build on this family. The catch for your pipeline: the signal lives in exact token choices, so anything that rewrites tokens attacks it.
Post-hoc edit watermarks and zero-width payloads
If you never touch the sampler, the text itself is your only channel. Post-hoc watermarks run a rewrite pass over finished output and store bits in the rewrite choices.
Synonym and syntactic rewrites
Synonym schemes replace selected words with keyed equivalents, and the bit is which synonym survived. Payload capacity is decent and stealth is limited, since systematic vocabulary swaps leave stylistic fingerprints. The scheme also inherits the family trade-off: downstream word-level edits overwrite your bits one swap at a time, and each lost synonym is a lost bit. Syntactic schemes do better by moving the payload into sentence structure. The EXPEDITO scheme encodes its message in tree-based rewrite decisions, so deleting or swapping individual words leaves the structural choices, and hence the bits, intact. The cost is a rewriting model in the loop plus fluency risk, because the same machinery that hides your mark can produce stilted sentences. Post-hoc work in this lineage treats that quality tax as the standing price of admission.
Zero-width character watermarks in Python
The degenerate but genuinely useful case is invisible Unicode. Zero-width characters carry bits that render as nothing, survive most plain copy-paste, and let you hide an arbitrary payload in ordinary-looking text.
def zw_embed(text, message):
bits = "".join(format(b, "08b") for b in message.encode())
payload = "".join("\u200b" if b == "0" else "\u200c" for b in bits)
i = text.find(" ")
return text[:i] + payload + text[i:] if i > 0 else text + payload
def zw_read(text):
return "".join("0" if c == "\u200b" else "1"
for c in text if c in "\u200b\u200c")
Notice what this code cannot defend against:
One pass of Unicode normalization (NFKC) or any filter that strips format-category characters erases the entire payload. Many platforms sanitize on paste, and screen readers and crawlers see noise you no longer do. Treat zero-width marks as leak tracing for intact copies, not as adversarial provenance.
Semantic invariant watermarks that survive paraphrasing
Paraphrasing is the attack that decides everything else in this article, so the family built for it deserves its own mechanics. Semantic invariant watermarking (the SIR approach) moves the signal from surface tokens to choices among semantically equivalent entities. For each slot in the text, a canonical set of interchangeable renderings exists: a role can be "the CEO" or "the chief executive," a plan can be "strong" or "solid." A keyed permutation decides which rendering encodes a 0 and which encodes a 1, generation emits the chosen variant, and verification maps the surface text back to the canonical entity to recover the bit. A paraphrase rewrites connective tissue, but the facts persist, and the bit lives in the facts.
import hashlib
SLOTS = {"quality": ["strong", "solid"], # interchangeable variants only
"cost": ["cheap", "low-cost"]}
def order_for(key, slot):
h = int(hashlib.sha256(f"{key}|{slot}".encode()).hexdigest(), 16)
return sorted(SLOTS[slot]), h % 2
def encode(text, bits, key):
for (slot, _), bit in zip(SLOTS.items(), bits):
order, off = order_for(key, slot)
text = text.replace(f"<{slot}>", order[off ^ bit])
return text
def decode(text, key):
out = []
for slot in SLOTS:
order, off = order_for(key, slot)
idx = next((i for i, v in enumerate(order) if v in text), None)
out.append(None if idx is None else idx ^ off)
return out
The costs are real. Generation needs an extra step, often an LLM call, to produce variants that genuinely fit the context, and verification needs to map rewritten text back to canonical entities, which is another LLM job when the paraphraser is aggressive. Equivalence is also an assumption: two variants you declared interchangeable may not be in context. What you buy is the property no other family offers, resistance to the attack that breaks the rest, which is why entity-level marks are the standing answer when paraphrase sits in your threat model.
A runnable robustness harness in Python
No API keys required. The harness defines three attacks and scores a watermark before and after each one, so every claim in this article is checkable against your own outputs.
import re, random, unicodedata
def attack_copy_paste(t):
t = unicodedata.normalize("NFKC", t) # the killer for zero-width bits
return re.sub(r"\s+", " ", t).strip()
def attack_edits(t, rate=0.15, seed=7):
rng, swap = random.Random(seed), {"strong": "robust",
"cheap": "frugal", "quick": "swift"}
return " ".join(swap.get(w, w) if rng.random() < rate else w
for w in t.split())
def attack_paraphrase(t):
# rule-based stand-in; swap in a real LLM paraphraser for honest testing
rules = [("a strong and cheap plan", "the plan was strong yet cheap")]
for a, b in rules:
t = t.replace(a, b)
return t
def survival(text, mark, verify, attacks):
marked = mark(text)
print(f"{'baseline':<10} {verify(marked)}")
for name, fn in attacks.items():
print(f"{name:<10} {verify(fn(marked))}")
Plug in any family: pass green-list detect as verify, or zw_read, or bit accuracy over the SIR decode. Two caveats keep results honest. The copy-paste function already includes NFKC normalization, because that is what real pipelines do to pasted text. And the paraphrase proxy is deliberately weak, so treat its results as an upper bound on survival and wire in an actual paraphrasing model before you trust a deployment decision to the harness.
What the survival tests actually show
Does AI text watermarking survive paraphrasing attacks? That question has no single answer; families do. Here is the qualitative matrix the harness reproduces.
| Family | Copy-paste | Word edits | Paraphrase |
|---|---|---|---|
| Green-list statistical | Survives, z dips | Graceful decline with edit rate | Can fall toward chance |
| Zero-width payload | Survives only unsanitized channels | Survives | Erased by normalization |
| Synonym post-hoc | Survives | Bits erode per swap | Mostly lost |
| Syntactic post-hoc | Survives | Holds better than synonym | Degrades |
| Semantic invariant | Survives | Survives if variants kept | Holds while facts survive |
Three readings matter. First, statistical marks degrade gracefully: every surviving token still votes green or red, so z falls with the surviving token count instead of collapsing, and light edits leave detection standing. Strong paraphrase is different. Sadasivan et al., 2023 show paraphrase pressure sharply degrading both detector reliability and watermark detectability, sometimes toward chance when the rewriting is aggressive. That is the honest ceiling of the green-list family, and the same paper is why you should treat post-hoc detectors as fragile in the wild.
Second, zero-width marks were never adversarial; they die at the first sanitizing paste, exactly as the harness shows in one line. Third, the families that resist paraphrase pay elsewhere: semantic marks need LLM assistance and careful slot design, syntactic marks accept fluency loss.
The deep pattern is the detectability-robustness dial. Every scheme lets you spend redundancy (more biased tokens, more slots, more bits) to survive stronger attacks, and every unit of redundancy costs quality, payload, or stealth. Text watermark robustness is a budgeted trade-off, not a property you can maximize for free.
Production watermarking with SynthID and Claude

The research families above already ship at scale. Google DeepMind's SynthID text watermarking is the largest public deployment: the SynthID-Text paper reports the scheme running across live Gemini traffic in a large-scale online experiment, with no measurable quality impact in their deployment setting, and Google has released the code as open source. Architecturally it is the production descendant of the green-list idea, tuned so the sampler bias stays statistically subtle while verification remains cheap.
Anthropic's disclosure landed in the same season. Anthropic's announcement confirms that recent Claude models are watermarked and that the mark is verifiable through Google DeepMind's SynthID Detector. For builders, that alignment matters: two major labs now emit generation-time watermarks, and at least one exposes a public verification path. The October 2025 White House provenance commitments, under which OpenAI, Anthropic, Google, Microsoft, Adobe, and others agreed to label AI-generated content and adopt C2PA-style credentials, push the same direction. If you build on these APIs you inherit watermarking rather than add it, and the remaining decision is what to do about your own models and pipelines.
When to watermark, detect, or sign metadata
Close the loops. The decision keys on what you control and who you fear.
| What you control | Right tool | Ceiling |
|---|---|---|
| The sampler (generation) | Keyed statistical or semantic watermark | Strongest option in the stack |
| A rewrite pass (output only) | Post-hoc tools that watermark AI-generated text | Copy-paste robustness at best |
| Distribution (files, feeds) | Signed C2PA content credentials | Proves origin until content leaves the container |
| Neither | Post-hoc AI detection | A statistical guess, brittle under paraphrase |
Threat model is the second axis. A casual copyist pasting your text into a forum is defeated by zero-width bits and even by weak statistical marks. A determined rewriter running everything through a paraphrasing model defeats everything except semantic invariant marks, and even those survive only while the facts survive. Framed as AI text detection vs watermarking, the answer resolves here: detection is the tool of last resort for text you did not generate, and the reliability research above says to hold it loosely.
A concrete build order for a team deciding today:
- Implement the green-list snippet on a model you control and measure detect z and output quality across a few hundred samples.
- Run the harness with a real paraphrasing model, not the rule proxy, and record your own survival matrix.
- If paraphrase is in your threat model, add a semantic slot layer for claims that must survive rewriting, and accept the extra LLM calls.
- Sign distribution artifacts with C2PA where you ship files, so provenance does not end at the copy button.
- Keep post-hoc detection for third-party text only, as triage rather than proof.
We built a statistical AI text detector in a previous piece, and it is the right baseline for the last row of the table. The pattern across every row is the thesis this article opened with. Watermark schemes differ in cleverness, but viability is decided by the pipeline stage you control, and paraphrasing is the attack that tells you which stage you needed.
Stay in the loop.
Get the latest posts and exclusive content delivered to your inbox.
Join 3 readers. No spam. Unsubscribe in one click, anytime.
About the author
Tyler Brooks
Tools Analyst
Tyler has tested developer tooling for a decade, first as a platform engineer and now as an independent analyst. He reviews models, frameworks, and APIs the way he would want them reviewed before relying on them for real work.
Related Posts
Prompt Dependency Graphs That Shrink Your Retest Set
Build a prompt dependency graph to compute the blast radius of any prompt change and rerun only the evals your multi-prompt LLM system needs.
NLP vs LLM vs RAG, Routed by Task Shape and Cost
NLP vs LLM vs RAG is a routing decision set by task shape. Compare the cost math, failure modes, and an LLM-fallback pattern before picking a model.
Build an AI Text Detector, Then Test If It Can Ship
Build an AI text detector with small local models, stress-test it on short and human-edited text, and use the error rates to decide if it ships.

