Shopify Variant Image CSV Validator: Find Mapping Risks Before Import
Rewarx practical lab · November 3, 2026 · Synthetic fixtures
Shopify Variant Image CSV Validator: Find Mapping Risks Before Import
This read-only CSV preflight reports missing image assignments, duplicate variant identities, suspicious cross-product references and malformed URLs before you import. It does not modify your file or call Shopify. It uses Python’s CSV parser, so quoted commas and multiline cells are handled as CSV rather than split text.
The current Shopify contract
Shopify’s current columns include URL handle, SKU, Product image URL and Variant image URL. Older aliases remain compatible. Product image URLs and variant image URLs serve different roles. Shopify warns that included blank columns can overwrite existing data; omitted columns behave differently. Option columns also have dependencies. Read the complete current CSV specification.
Treat community complaints as diagnostic leads. The variant-assignment discussion includes replies showing that expanding the variant list still permits assignment. It does not prove that Shopify removed per-variant images across the platform. An older CSV-import discussion describes a different reported failure and is not a current platform-wide status report.
What this validator checks
| Finding | Meaning | Review action |
|---|---|---|
| BLANK_IMAGE_REVIEW | A variant row has an included but blank image field | Determine whether the mapping is intentionally blank and whether overwrite is enabled. |
| DUPLICATE_VARIANT_KEY | Handle and option name/value tuple appeared earlier | Check that the CSV has not duplicated the same variant. |
| REUSED_SKU_REVIEW | SKU appears on more than one variant row | May be intentional; verify identity rather than deleting rows. |
| CROSS_PRODUCT_IMAGE_REVIEW | Image URL occurs only in another product gallery in this CSV | Suspicious, but not proof of the wrong image. |
| INVALID_URL | Syntactically invalid HTTP(S) URL | Fix syntax; reachability is a separate check. |
| OPTION_DEPENDENCY | Variant row lacks Option1 identity fields | Review official import dependencies before import. |
Run it on a copy of your export
python shopify_variant_csv_preflight.py products.csv --output findings.csvSave the full code below. Current column names are preferred; supported aliases include Handle, Variant SKU, Image Src and Variant Image. When both current and legacy names occur together, the report flags ambiguity. Additional gallery rows are not automatically treated as missing variants. Starting physical line numbers are reported, including when a quoted cell spans more than one line.
Complete CSV validator
"""Read-only Shopify CSV review. No network/API writes and no automatic fixes.
python shopify_variant_csv_preflight.py products.csv --output findings.csv
Rows identify physical starting line numbers (including multiline quoted cells).
"""
import argparse,csv
from collections import defaultdict
from pathlib import Path
from urllib.parse import urlsplit
ALIASES={'handle':['URL handle','Handle'],'sku':['SKU','Variant SKU'],
'gallery':['Product image URL','Image Src'],'image':['Variant image URL','Variant Image']}
for n in range(1,4):
ALIASES['name'+str(n)]=[f'Option{n} name',f'Option{n} Name']
ALIASES['value'+str(n)]=[f'Option{n} value',f'Option{n} Value']
def valid_url(v):
try:
u=urlsplit(v);return u.scheme in ('http','https') and bool(u.hostname) and not u.username and not u.password and not any(c.isspace() for c in v)
except ValueError:return False
def validate(path):
issues=[];rows=[]
def add(line,code,detail):issues.append({'line':line,'code':code,'detail':detail})
with open(path,newline='',encoding='utf-8-sig') as f:
reader=csv.DictReader(f);headers=reader.fieldnames or []
if len(set(headers))!=len(headers):add(1,'DUPLICATE_HEADER','Duplicate column names are ambiguous')
mapping={k:next((a for a in aliases if a in headers),None) for k,aliases in ALIASES.items()}
for k,aliases in ALIASES.items():
if sum(a in headers for a in aliases)>1:add(1,'AMBIGUOUS_ALIAS',k+': both current and legacy columns present')
if not mapping['handle']:add(1,'MISSING_HANDLE_HEADER','Use URL handle or legacy Handle')
if not mapping['image']:add(1,'IMAGE_COLUMN_OMITTED','Existing variant image field is not included; no image assignment supplied')
line=1
for raw in reader:
start=line+1;line=reader.line_num
if None in raw or any(v is None for v in raw.values()):add(start,'MALFORMED_ROW','Field count differs from headers')
r={k:(raw.get(col) or '').strip() if col else '' for k,col in mapping.items()};r['line']=start
r['variant']=bool(r['sku'] or any(r['value'+str(n)] for n in range(1,4)))
rows.append(r)
galleries=defaultdict(set)
for r in rows:
if r['gallery'] and r['handle']:galleries[r['gallery']].add(r['handle'])
keys={};skus={}
for r in rows:
line=r['line'];h=r['handle'];img=r['image']
if not h:add(line,'MISSING_HANDLE','Product identifier is blank; not inferred from the previous row')
for field in ['gallery','image']:
if r[field] and not valid_url(r[field]):add(line,'INVALID_URL',field+' is not a syntactically valid HTTP(S) URL')
if not r['variant']:continue # Additional gallery rows need not define a variant.
if mapping['image'] and not img:add(line,'BLANK_IMAGE_REVIEW','Missing assignment; when overwriting, a blank included field may clear existing data')
if not r['name1'] or not r['value1']:add(line,'OPTION_DEPENDENCY','Variant identity needs Option1 name and value; review before import')
for n in range(1,4):
if bool(r['name'+str(n)])!=bool(r['value'+str(n)]):add(line,'OPTION_PAIR','Option name/value pair incomplete: '+str(n))
key=(h,)+tuple((r['name'+str(n)],r['value'+str(n)]) for n in range(1,4))
if key in keys:add(line,'DUPLICATE_VARIANT_KEY','Same handle/options first appeared at line '+str(keys[key]))
else:keys[key]=line
if r['sku']:
if r['sku'] in skus:add(line,'REUSED_SKU_REVIEW','SKU also used at line '+str(skus[r['sku']])+': inspect intent')
else:skus[r['sku']]=line
if img in galleries and h not in galleries[img]:add(line,'CROSS_PRODUCT_IMAGE_REVIEW','URL occurs only in another product gallery in this CSV; may be intentional')
return issues
def main():
p=argparse.ArgumentParser();p.add_argument('csv');p.add_argument('--output',required=True);a=p.parse_args()
if Path(a.csv).resolve()==Path(a.output).resolve():raise SystemExit('Output must not replace input')
issues=validate(a.csv)
with open(a.output,'w',newline='',encoding='utf-8') as f:
writer=csv.DictWriter(f,fieldnames=['line','code','detail']);writer.writeheader();writer.writerows(issues)
print(f'{len(issues)} review findings; no changes made to input')
if __name__=='__main__':main()
Copyable synthetic fixture
URL handle,Title,SKU,Option1 name,Option1 value,Product image URL,Variant image URL
tee,"Tee, cotton",R-S,Color,Red,https://example.com/red.png,https://example.com/red.png
tee,,R-M,Color,Blue,,https://example.com/red.png
tee,,B-S,Color,Green,,
tee,,B-S,Color,Green,,file:///wrong.png
shoe,Shoe,SH,Color,Black,https://example.com/shoe.png,https://example.com/red.png
tee,,,,,https://example.com/detail.png,
Save as products.csv, then run the command above. The red image is intentionally reused for two option values: that alone creates no error. The tool cannot infer color meaning from a URL or pixels. It checks structural risk, leaving visual verification to a person.
Actual six-row fixture report
line,code,detail
4,BLANK_IMAGE_REVIEW,"Missing assignment; when overwriting, a blank included field may clear existing data"
5,INVALID_URL,image is not a syntactically valid HTTP(S) URL
5,DUPLICATE_VARIANT_KEY,Same handle/options first appeared at line 4
5,REUSED_SKU_REVIEW,SKU also used at line 4: inspect intent
6,CROSS_PRODUCT_IMAGE_REVIEW,URL occurs only in another product gallery in this CSV; may be intentional
Test results and limits
The combined image/CSV suite passed 15 tests on Python 3.9.6. CSV tests cover current and legacy columns, quoted commas, multiline line numbers, shared images, duplicate keys, blank fields, invalid URLs, gallery-only rows and cross-product references. The six-row demonstration produced five review findings. These are local synthetic results.
No development store import was performed. A clean report cannot establish Shopify import success, correct theme rendering, URL accessibility or semantic color accuracy. The cross-product check only knows galleries present in this file, not your full store. Missing SKU is not inherently an error: option identity still matters. Run the full published test suite.
Use the findings as a review queue
- Keep the exported original and avoid changing row order during review.
- Resolve structural duplicate identities first, then review intentional shared images.
- Treat blank included fields as a review trigger, not a command to invent replacements.
- Check the real image against the selected option values. A URL containing “red” does not prove a red product.
- Use a development store or controlled import preview before applying changes to production. This article has not verified that final step.
Related practical guides
- Check feed image dimensions
- Test metadata preservation
- Shopify CSV image-import troubleshooting
- Catalog tool workflow comparison
Sources checked November 3, 2026
Rewarx workflow entry
Inspect the Rewarx background-removal entry page. Its primary button leads to Studio authentication. The validators and metadata lab here run locally; this article does not claim the Studio performs these checks or preserves metadata automatically.
Check HTTP delivery before CSV import
An image URL may return HTML, a permission error or a redirect instead of the asset. Use the read-only image HTTP diagnostic and local server fixtures to record the response, then return to this guide for mapping checks. Rewarx already supports Shopify import; the local CSV preflight described here is a separate utility, and its fixture tests do not validate the existing product importer.