Headlines for ad creatives tend to get written in a document, approved in a chat thread and only seen at their real size once the image is exported. By then, fixing "The best desk lamp ever made!!" means another round of edits. This post walks through a small Python script that flags the headlines worth a second look before anyone opens an editor.
The rules come from AdMultiplier's short guide on how to write short headlines with a clear job: give the headline one useful detail, tie claims to evidence, and read it at the size the audience will see.

Summary of the linked page: admultiplier.org/blog/write-short-ad-headlines (read 2026-10-30)
What the script should (and should not) do
A linter cannot tell you whether a headline is good. It can tell you which ones break a rule you already agreed on. So the goal is narrow:
- count words against a house limit you choose per placement;
- flag claim words ("best", "guaranteed", "proven") so someone checks the evidence;
- flag urgency phrases that need a real deadline behind them;
- flag numbers and percentages, which need a source;
- flag headlines that are just the product name with nothing useful added.
It does not rewrite anything. Every flag is a question for a human.

The five checks and the question each one asks
The code
"""headline_lint.py - flag ad headlines that need a second look before they go on a creative."""
import re, sys, json
MAX_WORDS = 8 # your own house limit; change it per placement
CLAIM_WORDS = ["best", "#1", "number one", "guaranteed", "cure", "proven", "clinically", "never fails"]
URGENCY = ["hurry", "last chance", "act now", "don't miss", "only today"]
def lint(headline, product=""):
notes = []
words = re.findall(r"[\w#'%-]+", headline)
if len(words) > MAX_WORDS:
notes.append(f"{len(words)} words (house limit {MAX_WORDS}) - read it at creative size")
low = headline.lower()
for w in CLAIM_WORDS:
if w in low:
notes.append(f"claim word '{w}' - keep only if you have evidence for this exact claim")
for w in URGENCY:
if w in low:
notes.append(f"urgency phrase '{w}' - is there a real deadline?")
if re.search(r"\d+\s*%|\d{2,}", headline):
notes.append("number or percentage - attach the source before export")
if product and product.lower() in low and len(words) <= 3:
notes.append("headline is mostly the product name - add one useful detail")
if headline.count("!") > 1:
notes.append("more than one '!'")
return notes
if __name__ == "__main__":
rows = json.load(open(sys.argv[1], encoding="utf-8"))
for r in rows:
n = lint(r["headline"], r.get("product", ""))
print(("OK " if not n else "LOOK ") + r["headline"])
for x in n:
print(" - " + x)
The word limit is deliberately a constant at the top. Eight words is not a rule from anywhere; it is a placeholder. A square social image, a vertical story and a wide banner will each need their own number, and you should set it after looking at a few exports at real size.
Input and output
The input is a plain JSON list, the kind you can export from a spreadsheet of headline drafts:
[
{"headline": "A desk lamp for small workspaces", "product": "desk lamp"},
{"headline": "The best desk lamp ever made!!", "product": "desk lamp"},
{"headline": "Desk Lamp", "product": "desk lamp"},
{"headline": "Hurry, 70% brighter light for every desk in your home office today", "product": "desk lamp"},
{"headline": "Warm light that clips to a shelf", "product": "desk lamp"}
]
Running python3 headline_lint.py headlines.json prints:
OK A desk lamp for small workspaces
LOOK The best desk lamp ever made!!
- claim word 'best' - keep only if you have evidence for this exact claim
- more than one '!'
LOOK Desk Lamp
- headline is mostly the product name - add one useful detail
LOOK Hurry, 70% brighter light for every desk in your home office today
- 12 words (house limit 8) - read it at creative size
- urgency phrase 'hurry' - is there a real deadline?
- number or percentage - attach the source before export
OK Warm light that clips to a shelf
The first headline is the example used in the guide itself: one concrete detail ("for small workspaces") and no promise. The fourth one shows why the checks stack: a long line, a made-up urgency and a percentage with no source behind it.

Lint the list first, then design only the headlines that pass or have been justified
Where it fits in a creative workflow
The useful moment for this script is between "list of headline ideas" and "put them on the creative". In practice:
- Draft headlines in a sheet, one row per idea, with the product name in its own column.
- Export to JSON and run the linter.
- For each
LOOKline, either rewrite it or write down the evidence that justifies the flagged word or number. - Only then place the surviving headlines on the creative and read them at their real size, which is the step the guide stresses: a headline that works in a document may feel crowded on the image.
If you build variations with a template tool such as AdMultiplier, the cleaned list is what you take into the editor, one headline per variation.
Extending it
A few additions that are easy to make without turning it into a different tool:
-
Per-placement limits. Replace
MAX_WORDSwith a dict such as{"square": 8, "story": 6}and add aplacementfield to each row. - Your own banned list. Regulated categories (health, finance) will have words that need legal review. Keep them in a separate file so the marketing and legal lists do not get mixed up.
-
Exit code for CI. If headlines live in a repo,
sys.exit(1)when any row isLOOKturns this into a pre-merge check. - Repetition check. The guide suggests removing words that repeat the product image. A simple version compares the headline with a short description of the image and flags shared nouns.

What still needs a human after the linter passes
Testing the linter itself
If the script becomes part of your workflow, give it a few tests so a future edit does not silently change its behaviour. A minimal pytest file:
from headline_lint import lint
def test_clean_headline_passes():
assert lint("A desk lamp for small workspaces", "desk lamp") == []
def test_claim_word_is_flagged():
assert any("best" in n for n in lint("The best lamp", "lamp"))
def test_name_only_is_flagged():
assert any("product name" in n for n in lint("Desk Lamp", "desk lamp"))
These three cases mirror the examples above. When you add a new rule, add one passing and one failing headline for it. It also documents, for whoever picks up the script next, what each rule is supposed to catch.
What it cannot check
Passing the linter does not mean a headline is ready. Spelling of product names, offer conditions, image rights and how the line breaks on the final creative still need a person looking at the exported file. Treat the script as a way to make that review shorter, not to skip it.
Top comments (0)