Before you import a product CSV into Shopify, it helps to know which products have no price or no image. Shopify may accept the file anyway, and you only notice later that a product shows up without a picture or at 0.00.
This script reads a product CSV and lists every product that has a missing, zero or unreadable price, or no image at all. It only reads the file and never changes it. It uses only Python's standard library.
One product, several rows
In Shopify's product CSV a product can span several rows: one per variant, plus extra rows that only carry another image. So "no image" cannot be checked per row. The script groups rows by Handle and asks two questions per product: does at least one row have a price, and does at least one row have an image URL?
The script
#!/usr/bin/env python3
import argparse
import csv
import sys
REQUIRED = ["Handle", "Variant Price", "Image Src"]
def read_rows(path):
with open(path, newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
missing = [c for c in REQUIRED if c not in (reader.fieldnames or [])]
if missing:
sys.exit(f"Missing column(s): {', '.join(missing)}")
return list(reader)
def check(rows):
"""Return {handle: [problems]} for products with no price or no image."""
products = {}
for row in rows:
handle = row["Handle"].strip()
if not handle:
continue
p = products.setdefault(handle, {"title": "", "prices": [], "images": 0})
if row.get("Title"):
p["title"] = row["Title"]
price = (row.get("Variant Price") or "").strip()
if price:
p["prices"].append(price)
if (row.get("Image Src") or "").strip():
p["images"] += 1
problems = {}
for handle, p in products.items():
found = []
if not p["prices"]:
found.append("no price")
else:
for price in p["prices"]:
try:
if float(price.replace(",", ".")) <= 0:
found.append(f"price {price}")
break
except ValueError:
found.append(f"unreadable price {price!r}")
break
if p["images"] == 0:
found.append("no image")
if found:
problems[handle] = found
return products, problems
def main():
parser = argparse.ArgumentParser()
parser.add_argument("csv_file")
args = parser.parse_args()
products, problems = check(read_rows(args.csv_file))
print(f"{len(products)} products checked, {len(problems)} with problems")
for handle, found in sorted(problems.items()):
print(f" {handle}: {', '.join(found)}")
if __name__ == "__main__":
main()
Trying it
I tested it on a small made-up file (not real shop data):
Handle,Title,Variant Price,Image Src
blue-mug,Blue Mug,12.50,https://example.com/a.jpg
blue-mug,,,https://example.com/b.jpg
red-mug,Red Mug,,https://example.com/c.jpg
tote-bag,Tote Bag,19.90,
sticker-pack,Sticker Pack,0,https://example.com/e.jpg
poster-a3,Poster A3,"9,90",https://example.com/f.jpg
notebook,Notebook,abc,
$ python3 check_missing.py sample.csv
6 products checked, 4 with problems
notebook: unreadable price 'abc', no image
red-mug: no price
sticker-pack: price 0
tote-bag: no image
The blue mug is fine even though its second row has no price: the image row belongs to the same product, and the first row has a price. The poster with the price 9,90 is accepted because the script also reads a comma as the decimal separator. If a column is missing from the file, the script stops with a message instead of guessing.
Limits
- It checks only price and image URL presence. It does not test whether the image URL actually loads.
- Variants of one product are treated together: if only one variant has a price, the product passes. Check per variant if that matters for your shop.
- Column names can differ between exports. Check your own file's header first.
An easy next step is to run it automatically before every import and stop if the list is not empty.
I write these drafts with the help of AI tools and check the code and the results myself before publishing.
Top comments (0)