Shopify rejects or silently merges product rows when the Handle column is wrong. Before importing, I run a small check that needs only the standard library.
What it checks
- empty handles
- handles that are not URL-safe (spaces, capitals, underscores)
- a handle that comes back after other products (variants must be contiguous)
- two rows with a title for the same handle
The script
import csv, re, sys SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") def check(path): problems = [] seen = {} with open(path, newline="", encoding="utf-8-sig") as f: reader = csv.DictReader(f) last_handle = None closed = set() for line, row in enumerate(reader, start=2): handle = (row.get("Handle") or "").strip() title = (row.get("Title") or "").strip() if not handle: problems.append((line, "empty handle")) continue if not SLUG.match(handle): problems.append((line, f"handle not URL-safe: {handle!r}")) if handle != last_handle: if handle in closed: problems.append((line, f"handle {handle!r} reappears after other products")) if last_handle is not None: closed.add(last_handle) last_handle = handle if title: if handle in seen and seen[handle] != line: problems.append((line, f"second product row with title for handle {handle!r} (first at line {seen[handle]})")) else: seen[handle] = line return problems if __name__ == "__main__": probs = check(sys.argv[1]) for line, msg in probs: print(f"line {line}: {msg}") print(f"{len(probs)} problem(s)") sys.exit(1 if probs else 0) Test run
With a small CSV containing one bad handle, one split product and one empty handle:
line 4: handle not URL-safe: 'Red Mug' line 5: handle 'blue-mug' reappears after other products line 5: second product row with title for handle 'blue-mug' (first at line 2) line 7: empty handle 4 problem(s) The exit code is 1 when problems are found, so you can use it in a script or CI step.
Notes
-
utf-8-sighandles the BOM that Excel adds. - Line numbers match the spreadsheet row, since the header is line 1.
- It does not check images, prices or SKUs; that is a separate check.
Free small scripts like this live in my repo: https://github.com/skeil1996-ui/python-automation-scripts-free
![[ANN] PgLoom - Deterministic race-condition model checker on the PostgreSQL wire protocol](https://us1.discourse-cdn.com/flex019/uploads/rust_lang/original/3X/d/c/dc80f49bdcee4484d529dde8b7ec560de189f6d5.gif)