Text Watermarking in Python: Catch Whoever Copies Your Writing
AI companies quietly watermark billions of words a day. Here's how to apply the same three families of techniques to your own writing-and what real experiments reveal about which watermarks survive copy-paste, editing, and paraphrasing.
The post Text Watermarking in Python: Catch Whoever Copies Your Writing appeared first on Towards Data Science.
1. Who This Is For, and What You'll Get
If you publish writing online and want more than a guess when it gets copied, this is for you. You'll get:
- A simple coin-flip intuition for three types of text watermarking.
- An 80-line, standard-library-only script that embeds a 32-bit ID in text and detects it later.
- A keyed word-choice watermark, a model-free detector, and a meaning-aware upgrade-plus where they fall short in practice.
- Real-world survival tests across 12 channels, several editing attacks, full paraphrasing, and Chinese translation.
- A practical rule for choosing the right watermark for the threat.
Everything was tested on real models: Gemma-2-9b-it for watermarking and Qwen2.5-7B-Instruct for attacks, running on an NVIDIA GB10. The test set included 50 original paragraphs and 100 public-domain passages for false-positive checks. The scripts, corpus, and calibration data are all in text-watermarking-toolkit, so you can reproduce the results yourself.
2. People Have Been Watermarking Text for a Century
Long before watermarking became cryptographic, it was used in a much simpler way: hide a tiny, deliberate mistake or variation, then see who copies it.
2.1 Trap Streets
One example is from mapmakers. Mapmakers inserted fake streets, towns, or landmarks into maps. If the same fake feature appears on a competitor's map, the source of the copying is obvious. In 1925, the General Drafting Company added a fake New York town called Agloe, named from its founders' initials. Years later, a store opened at the crossroads and adopted the name. The fictional town had effectively become real.
2.2 Mountweazels
Reference books use the same trick with fake entries. The New Columbia Encyclopedia famously included Lillian Virginia Mountweazel, a fictional photographer with an elaborate biography. The New Oxford American Dictionary later planted esquivalience, supposedly meaning "the wilful avoidance of one's official responsibilities." Neither was real. Both were bait: if another reference work reproduced them, that was evidence of copying.
2.3 Canary Traps
A canary trap takes the idea one step further. Instead of giving everyone the same fake detail, each recipient gets a slightly different version. If the document leaks, the variation identifies whose copy it came from. Elon Musk has said Tesla used this technique in 2008 by varying whether sentences were separated by one space or two. Those tiny differences formed a binary signature unique to each recipient.
2.4 Genius vs. Google
The most important example here is one where the watermark apparently worked-but the lawsuit still failed. Lyrics site Genius suspected Google was reproducing its transcriptions in search results. Genius began alternating straight and curly apostrophes in a pattern that, when read as Morse code, spelled REDHANDED. The pattern was seeded into 301 songs and reportedly appeared in Google's results for 116 of them. Genius later used a second watermark based on different types of spaces, encoding the word Genius. It sued Google for $50 million in 2019. The case was dismissed in 2020, not because the watermark failed, but because Genius did not own the underlying lyrics; it licensed them. That distinction matters: a watermark can show that your version of a text was copied. It cannot, by itself, prove that you owned the text in the first place.
3. The Coin-Flip Analogy
Imagine that while writing, you repeatedly make a hidden binary action: insert one of two invisible characters, choose between two synonyms, or pick between two equally plausible next words. One matching choice proves nothing: it has a 50% chance of happening by accident. But if 30 choices match the key in a row, the odds of that happening randomly are roughly 1 in a billion. That probability is what gives the watermark statistical evidence. In formal terms, it can be expressed as a p-value.
The three watermarking families mainly differ in what the "coin flip" represents:
| Family | The coin | Where it lives |
|---|---|---|
| Invisible characters | a zero-width character - present or absent | between the letters |
| Keyed word choices | "big" vs. "large"; straight vs. curly apostrophe | in the words |
| Rigged sampling | which token the model emits next | inside the generator |
The first two can be applied to text that already exists. The third must happen while a language model is generating the text.
3.1 Technique 1 - Invisible Characters
Unicode includes characters that occupy zero visible space. Two examples are:
U+200B- zero-width spaceU+200C- zero-width non-joiner
Both are legitimate Unicode control characters, but in ordinary Latin text they are effectively invisible. Assign one character to 0 and the other to 1, and you have a hidden binary channel inside normal-looking text. Zach Aysan described this technique in 2017 and pointed out an important consequence: copied text can carry an invisible fingerprint with it. Someone pasting a leaked document elsewhere could unknowingly preserve the identifier embedded inside it.
How the Implementation Stays Reliable
Here's the implementation that I ran the experiments with. Three choices do most of the work:
- A checksum prevents false matches. Without validation, any random sequence of zero-width characters could decode into an apparent ID. Adding an 8-bit CRC reduces the chance of a random payload passing validation to about 1 in 256. Across 1,700 extraction attempts in these experiments, there were zero false IDs.
- The payload is repeated. A 40-bit identifier requires 40 invisible characters. Repeating it after each sentence means even a copied paragraph can still contain a complete, decodable mark.
- The decoder uses sliding windows. Copying can merge, truncate, or misalign invisible-character sequences. Instead of assuming perfect formatting, the decoder tests every possible 40-bit window and returns the valid ID that appears most often.
What It Looks Like
Before:
The report is confidential. Please do not forward it to anyone outside the team.
After:
Visually, it looks identical. Internally, the text contains repeated zero-width characters encoding the identifier.
extract() β 0xc0ffee01
In this example, 80 invisible characters were added with no visible change. When the text is copied, those characters can travel with it because the clipboard treats them as ordinary text.
To find out, I tested 150 marked paragraphs per channel across 11 mechanical transformations and one language model, checking whether the exact 32-bit ID survived.
What Survives
Against systems that simply move or reformat text, the watermark was extremely robust: 150/150 recovered through every tested mechanical channel, including:
- copying only the middle 50% of a paragraph
- UTF-8 and UTF-16 conversions
- JSON and HTML round trips
- Markdown and
.docxwhitespace normalization - Unicode NFKC normalization
- lowercasing
Two results are worth noting. Python's \s does not remove U+200B, because it is a Unicode format character rather than a normal space. NFKC normalization leaves it intact too.
What Kills It
- Explicit sanitization: 0/150 survived. If someone knows which zero-width characters to remove, a simple replacement strips the watermark completely.
- Language-model cleanup: 4/50 survived - just 8%. I only asked the model to fix typos and formatting while preserving the wording. But language models generally regenerate text rather than edit the original character stream, so the invisible characters disappear.
- With full paraphrasing, survival went down to 0/50.
That gives technique 1 a simple limit: it survives text transport. It does not survive text rewriting.
3.2 Technique 2 - Keyed Word Choices
The second technique hides information inside the wording itself rather than between characters. In other words, we write constantly equivalent choices:
big/largebegin/startproblem/issue
In normal practice, a writer chooses whichever sounds best. However, with a keyed watermark, a secret key determines which acceptable alternative to use. The major advantage over zero-width characters is that there is nothing extra to strip. The watermark is part of the text itself. Sanitizers, Unicode normalization, scrapers, and file conversions cannot remove it. To destroy the signal, you generally have to rewrite the words themselves.
3.2.1 The Mechanism
Start with a public table of interchangeable word pairs. At each eligible word, use a secret key to choose which member of the pair should appear. If the bit is 0, use the first word. If it is 1, use the second. Existing words that already match the key stay unchanged; mismatches are swapped only when the sentence still reads naturally.
With the wrong key-or ordinary unmarked text-each choice should match the key about 50% of the time. A marked document produces an unusually high number of matches. Two details matter:
- Use stable context. The key cannot depend on a nearby word that might itself be swapped, or where embedding one bit changes the next one. I instead use the nearest preceding word that is not in the synonym table.
- Hash the pair identity too. Hashing only the context creates correlations when the same context repeats. Including the word pair keeps slots independent enough for the binomial test to behave properly.
After that fix, 100 human-written passages tested under 20 keys produced null z-scores with mean 0.007 and standard deviation 1.020-almost exactly what the statistical model predicts.
3.2.2 How Well Does It Work?
I tested 50 paragraphs under three keys, using Gemma-2-9B-IT to reject swaps that changed meaning or grammar. It accepted 880 of 1,039 swaps (84.7%), changing about 2.72% of all words. At a calibrated 1% false-positive rate, detection required roughly z β₯ 2.45.
And here's the result with an interesting view:
| Text length | Slots | Mean z | Detected at 1% FPR |
|---|---|---|---|
| One paragraph (~215 words) | 12.9 | +2.93 | 73.3% (per-seed 68-78%) |
| One article (~1,075 words) | 64.3 | +6.66 | 100.0% (per-seed 100%) |
| Unmarked human text | - | β0.27 | 0.0% |
The main limitation is sample size. A single paragraph had only about 13 usable slots. Even a perfect 13-for-13 match reaches only about z = 3.6, leaving little room for noise. Across five paragraphs, though, the average rose to 64 slots, with z around +6.66 and perfect detection in the test set. The practical takeaway: word-choice watermarks work at document length, not tweet length.
3.2.3 Detection Ratio Across Techniques
| Attack | Detected | Mean z |
|---|---|---|
| Clean copy | 73.3% | +2.93 |
| Sentence deletion 30% | 53.3% | +2.48 |
| Word replacement 10% | 42.7% | +2.22 |
| Partial copy (middle 50%) | 28.7% | +1.88 |
| Word replacement 30% | 10.0% | +1.13 |
| Full paraphrase | 0.0% | β0.29 |
Deleting 30% of sentences still left about half the paragraphs detectable because surviving slots kept their original bits. Partial copying performs worse mostly because there are too few observations. Half a paragraph may contain only six slots, and six coin flips cannot produce enough statistical evidence to cross the detection threshold. In contrast, a full paraphrase is different. Detection fell to 0%, with mean z = β0.29-essentially the null distribution because the paraphraser made fresh word choices, effectively re-flipping every coin.
3.2.4 The Hidden Cost: Text Quality
The bigger problem is that a technically valid synonym is not always a good replacement. Examples from the experiment included:
- "Two dollars of filtered water repaired a issue I had thrown multiple hundred dollars at." (original: fixed a problem I had thrown several hundred dollars at)
- "a water heater failed and price me a ceiling" (original: cost me a ceiling)
The substitutions are understandable, but obviously worse. cost β price, for example, works in some noun contexts and fails as a verb. To measure that cost, I used Qwen2.5-7B-Instr
Comments
No comments yet. Start the discussion.