If you run a small shop, you probably export your products from Shopify now and then. Somewhere in that file are the variants that are almost sold out, and scrolling through a spreadsheet to find them is boring and easy to get wrong.
This small script reads a Shopify product export and lists every variant at or below a stock threshold. It only reads the file and never changes it. It uses nothing but Python's standard library.
What the export looks like
Shopify's product CSV has one row per variant. Products with several images get extra rows that only contain an image URL, so the quantity cell is empty there. The columns we need are:
-
Handle(the product ID in the URL) -
Title(only filled on the first row of a product) Variant SKUVariant Inventory Qty
The script
#!/usr/bin/env python3
import argparse
import csv
import sys
QTY_COLUMN = "Variant Inventory Qty"
SKU_COLUMN = "Variant SKU"
HANDLE_COLUMN = "Handle"
TITLE_COLUMN = "Title"
def read_rows(path):
with open(path, newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
missing = [c for c in (HANDLE_COLUMN, QTY_COLUMN) if c not in (reader.fieldnames or [])]
if missing:
sys.exit(f"Missing column(s): {', '.join(missing)}")
return list(reader)
def find_low_stock(rows, threshold):
low, unreadable = [], []
last_title = {}
for number, row in enumerate(rows, start=2): # row 1 is the header
handle = row[HANDLE_COLUMN]
if row.get(TITLE_COLUMN):
last_title[handle] = row[TITLE_COLUMN]
raw = (row.get(QTY_COLUMN) or "").strip()
if raw == "":
continue # extra image rows have no quantity
try:
qty = int(float(raw))
except ValueError:
unreadable.append((number, handle, raw))
continue
if qty <= threshold:
low.append({
"row": number,
"handle": handle,
"title": last_title.get(handle, ""),
"sku": row.get(SKU_COLUMN, ""),
"qty": qty,
})
return low, unreadable
def main():
parser = argparse.ArgumentParser()
parser.add_argument("csv_file")
parser.add_argument("--threshold", type=int, default=5)
parser.add_argument("--out", default="low_stock.csv")
args = parser.parse_args()
rows = read_rows(args.csv_file)
low, unreadable = find_low_stock(rows, args.threshold)
with open(args.out, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["row", "handle", "title", "sku", "qty"])
writer.writeheader()
writer.writerows(sorted(low, key=lambda r: r["qty"]))
print(f"{len(rows)} rows read, {len(low)} at or below {args.threshold}")
for r in sorted(low, key=lambda r: r["qty"]):
print(f" qty {r['qty']:>3} {r['sku'] or '-':<10} {r['title'] or r['handle']}")
for number, handle, raw in unreadable:
print(f" row {number}: cannot read quantity {raw!r} ({handle})")
if __name__ == "__main__":
main()
Trying it
I tested it on a small made-up file (not real shop data):
Handle,Title,Variant SKU,Variant Inventory Qty,Image Src
blue-mug,Blue Mug,MUG-B,12,https://example.com/a.jpg
blue-mug,,,,https://example.com/b.jpg
red-mug,Red Mug,MUG-R,3,https://example.com/c.jpg
tote-bag,Tote Bag,BAG-1,0,https://example.com/d.jpg
sticker-pack,Sticker Pack,STK-1,five,https://example.com/e.jpg
poster-a3,Poster A3,POS-A3,5,https://example.com/f.jpg
notebook,Notebook,NB-1,40,https://example.com/g.jpg
$ python3 low_stock.py sample.csv --threshold 5
7 rows read, 3 at or below 5
qty 0 BAG-1 Tote Bag
qty 3 MUG-R Red Mug
qty 5 POS-A3 Poster A3
row 6: cannot read quantity 'five' (sticker-pack)
The sticker pack has a typo (five instead of 5). The script does not guess; it reports the row number so you can fix it. The empty image row of the blue mug is skipped on purpose. With --threshold 2 only the tote bag is listed. The file low_stock.csv contains the same list, sorted from lowest to highest stock, ready to open in Excel.
Notes and limits
- It checks the quantity column only. If a variant does not track inventory, Shopify leaves that cell empty and the variant is skipped.
- The threshold is a plain number. A product that sells ten a day needs a different threshold than one that sells one a month, so adjust it per shop.
- Check the column names in your own export. Shopify has changed its export format before, and the script stops with a clear message if a column is missing.
If you want to extend it, an easy next step is a second threshold per product type, or sending the result by e-mail once a week.
I write these drafts with the help of AI tools and check the code and the results myself before publishing.
Top comments (0)