Restaurant Listing CSV Linter: Required, Length, Banned Terms, Overlap
StripeSoftware Engineer · Technical ScreenLast updated
Write a configurable linter for restaurant-listing CSV rows that reports missing fields, length violations, whole-word banned terms, and too-low or too-high word overlap between fields after normalization. Tests robust CSV parsing, text normalization, rule-driven design and edge-case thinking.
A food-delivery marketplace onboards restaurants from partner CSV exports. Before a batch is imported, a linter checks every row and reports which rules it breaks. The linter only reports problems. It never fixes, drops, or reorders rows.
def lint(csv_text: str, cfg: LintConfig) -> list[tuple[int, list[str]]]:
"""One entry per data row: (1-based data row number, failure codes in rule order)."""
All the rule parameters live in a config object, and nothing is hard-coded:
@dataclass
class LintConfig:
required: list[str] # fields that must be non-blank
lengths: dict[str, tuple[int, int]] # field -> (min, max) after trimming
banned: list[str] # words or phrases, matched as whole words
ignore_words: set[str] # dropped before similarity comparison
similarity: list[tuple[str, str, float | None, float | None]] # (field_a, field_b, min, max)
Part 1: field checks
For every row, in this order:
MISSING:<field>for each required field that is absent or only whitespace.LENGTH:<field>for each field inlengthsthat is present but whose trimmed length is outside[min, max].BANNED:<field>:<term>for each non-blank field and each banned term found in it. Matching is case-insensitive and whole-word, so"best"matches"Best tools"but not"bestseller". A phrase like"money back"matches across any spacing or punctuation.
The CSV has a header row. Quoted values can contain commas, so use a real CSV parser. A row with the wrong number of columns reports only MALFORMED.
Part 2: similarity between fields
Normalize text by Unicode NFKC, casefold, replace every non-alphanumeric character with a space, split, and remove ignore_words. Compare the two word sets with Jaccard similarity (|A∩B| / |A∪B|). For each (a, b, lo, hi) rule where both fields are non-blank and both word sets are non-empty: report LOW_OVERLAP:a~b if the similarity is below lo, and HIGH_OVERLAP:a~b if it is above hi.
Example
id,display_name,legal_name,tagline
101,Blue Fern Bakery,Blue Fern Bakery LLC,"Sourdough, pastries, and coffee"
102, ,Harbor Tools Inc.,Best tools
103,QuickFix,Northside Appliance Repair Co,Guaranteed results or your money back
104,Moss & Stone,"moss and stone, ltd",Moss & Stone
105,Lone Row
cfg = LintConfig(
required=["id", "display_name", "legal_name"],
lengths={"display_name": (2, 30), "tagline": (5, 40)},
banned=["guaranteed", "money back", "best"],
ignore_words={"llc", "inc", "co", "ltd", "and", "the"},
similarity=[("display_name", "legal_name", 0.5, None),
("tagline", "display_name", None, 0.8)],
)
lint(text, cfg) == [
(1, []),
(2, ["MISSING:display_name", "BANNED:tagline:best"]),
(3, ["BANNED:tagline:guaranteed", "BANNED:tagline:money back",
"LOW_OVERLAP:display_name~legal_name"]),
(4, ["HIGH_OVERLAP:tagline~display_name"]),
(5, ["MALFORMED"]),
]
Hints
Hint 1
Use the csv module. A quoted value with commas in it will break a hand-written split.
Hint 2
Write one normalization function (NFKC, casefold, punctuation to spaces, split) and use it for both banned terms and overlap.
Hint 3
To match a phrase as whole words, compare word tuples against contiguous slices of the field's word list.
Hint 4
Decide what happens when a word set is empty after removing ignored words, before you divide by zero.
Follow-up questions
- The file is 20 GB. How do you stream it and still report results in order?
- Product wants fuzzy banned-term matching (e.g. 'guarant33d'). How would you approach it without too many false positives?
- How would you let non-engineers add new rule types through config alone?
- How would you report column positions or suggested fixes alongside each failure code?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.