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-sig handles 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