Finally Accessible: Efficiently Populate ALT Texts with AI
Finally Accessible: Efficiently Filling ALT Texts with AI Minutes of reading time remaining By Antonio Blago September 14, 2025
Python, SEO, Tutorial
Practical Example in Shopify
In my job as an SEO freelancer for larger e-commerce shops like PURELEI, I was faced with the task of filling almost 40,000 ALT texts for images after a relaunch. So I tested dozens of Shopify apps—but none of them could reliably handle the work at this scale and to my quality standards. Shouldn't this have been solved by 2025?
It's just not that simple. Large image files over 10 MB, exotic formats like SVG or GIF—all of this makes direct use of the OpenAI API impossible. So I built a workaround with Python that automatically processes the data and imports the AI-generated texts directly into Shopify.
In the following how-to, I'll show you step by step how you can use this solution for your shop.
Why ALT Texts Are Important
ALT texts are much more than just a mandatory task in e-commerce. They ensure that screen readers can describe images, improve your visibility in Google Image Search, and serve as a fallback if an image doesn't load. With precise ALT texts, you support both accessibility and SEO.
My Experience with Apps—and Why They Weren't Enough
Before I developed my own solution, I tried countless apps from the Shopify App Store. But it quickly became clear: none of them could do the job the way a professional shop needs.
Many apps had strict limits and could only process a small number of images at a time
With larger data volumes, they became unstable or crashed
The automatically generated ALT texts were often too generic—examples like "product image" or "photo of jewelry" help neither SEO nor your users
Non-transparent pricing models: credits expired without it being clear when limits would reset. Support was usually not really helpful
The result: a lot of time invested, but no solution that met the high standards for SEO, accessibility, and scalability.
The Solution: Custom Automation with AI
At this point, I decided to develop my own solution. With a combination of the Shopify API and AI-powered text generation, you can create precise, consistent, and SEO-friendly ALT texts fully automatically.
The principle is simple:
Retrieve all product images from Shopify
Feed the AI with relevant product information (product name, material, color, collection, etc.)
Have a concise ALT text generated
Write the ALT text directly back into the image via API
Rules for Good ALT Texts
To ensure the texts are truly meaningful, I defined some clear rules:
Maximum 125 characters
Descriptive and specific (material, color, product type)
No filler words like "beautiful image of..."
Integrate keywords—but without spam
No repetitions
Example: ❌ "Jewelry" ✔️ "PURELEI charm pendant water lily made of sterling silver, worn on the ear"
The How-to: Step by Step
Overview
1. Collect Products and Images
Query all product images via the Shopify API and store them in a local database.
2. Call AI
Use a prompt containing all product info to generate concise ALT texts.
3. Save ALT Texts
Store the results in a local database like SQLite-DB so nothing gets lost.
4. Write Updates to Shopify
Use the API to write the generated ALT texts directly into the respective images.
5. Synchronize Database
Details
1. Collect Products and Images
Goal Retrieve all product images from Shopify, structure them cleanly, and store them locally.
How
The script fetches products via iter_products, extracts images, and builds ImageRef objects for them.
Relevant fields: product_id, image_id, src, alt, product_title, vendor, variants.
Unsupported formats are filtered out directly with is_supported_image, such as svg.
Database
db_init creates the jobs table.
When collecting, entries are only written during processing, keeping the DB lean.
You can optionally add a pure inventory table if you want a complete snapshot:
CREATE TABLE IF NOT EXISTS inventory( product_id INTEGER, image_id INTEGER, src TEXT, alt_shop TEXT, title TEXT, vendor TEXT, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(product_id, image_id) );
To fill, directly after collect_images():
for ref in all_refs: conn.execute(""" INSERT OR REPLACE INTO inventory(product_id,image_id,src,alt_shop,title,vendor) VALUES(?,?,?,?,?,?) """,(ref.product_id,ref.image_id,ref.src,ref.alt,ref.product_title,ref.vendor)) conn.commit()
Checks
Count total images, log per batch.
Filter empty src and duplicate image_id.
Check if ONLY_EMPTY is active if you only want to fill missing ALT texts.
2. Call AI
Goal Generate concise, clean ALT texts from product context.
How
build_context fetches material and colors from variants, plus vendor and tags.
prompt_alt builds a concise, rule-based prompt.
call_openai_alt calls the model, respects rate limits, validates the response, and truncates to ALT_MAX.
Quality Rules
Maximum 125 characters, no filler text, clear naming of product type, material, color.
Language can be controlled via ALT_LANG, for example de or en.
JSON enforcement in the response, robust fallback with regex in case the model gets chatty.
Error Cases
429 or network errors, retry is active.
Empty response, then abort with a clean error message.
Unsupported image formats are already filtered before the AI stage.
3. Save ALT Texts
Goal: Traceability, resumption, reporting.
How
Every decision is stored in jobs via db_upsert.
Columns: alt_old, alt_new, status, error, ts.
Typical status values: dry_run, updated, skipped_has_alt, skipped_unsupported, error.
Useful queries
-- All error cases SELECT * FROM jobs WHERE status='error' ORDER BY ts DESC;
-- Check suggestions from the dry run SELECT product_id,image_id,src,alt_old,alt_new FROM jobs WHERE status='dry_run' LIMIT 100;
-- Still open cases SELECT COUNT(*) FROM jobs WHERE status NOT IN ('updated','skipped_has_alt','skipped_unsupported');
4. Write Updates to Shopify
Goal: Reliably bring the generated ALT texts back into the system.
How
update_image_alt sends a PUT to /admin/api/{API_VERSION}/products/{product_id}/images/{image_id}.json with payload {"image":{"id":image_id,"alt":alt_new}}.
Successful updates are logged with status='updated'.
Safe procedure
First run with DRY_RUN=true, check entries by spot check.
Then DRY_RUN=false, small batches, for example BATCH_SIZE=50.
Observe rate limits, better to run constantly than in parallel, for stability.
Typical errors and causes
404, image or product deleted.
403, missing permissions, check admin token.
422, invalid payload, usually wrong ID or formatting.
5. Synchronize Database
Goal: Keep DB and Shopify in sync, detect manual changes, resume without duplicate work.
Sync recipe
Pull current state
Before each run, execute collect_images again to read the current ALT texts from Shopify.
Optionally update the inventory table, see above.
Determine delta
Join jobs and the fresh inventory state, detect manual changes.
Example: An employee has adjusted an ALT text in the backend, this should not be overwritten.
-- Find manual changes since the last run WITH last AS ( SELECT product_id,image_id,MAX(ts) AS last_ts FROM jobs GROUP BY product_id,image_id ) SELECT i.product_id,i.image_id,i.alt_shop AS alt_now,j.alt_new AS alt_suggest FROM inventory i LEFT JOIN jobs j ON j.product_id=i.product_id AND j.image_id=i.image_id LEFT JOIN last l ON l.product_id=i.product_id AND l.image_id=i.image_id AND j.ts=l.last_ts WHERE j.status='updated' AND i.alt_shop IS NOT j.alt_new;
Conflict strategy
If alt_now is not equal to alt_suggest, then
either respect the manual change and mark status='diverged_manual',
or only write back if alt_now is empty.
Rules as configuration, e.g. RESPECT_MANUAL_CHANGES=true.
Build reprocess queue
Reschedule open or failed jobs.
SELECT product_id,image_id FROM jobs WHERE status IN ('error') -- or other statuses ORDER BY ts ASC;
Ensure idempotency
The script skips already completed entries, see done set in main.
On resumption, the pipeline remains stable, no duplicate updates.
Practical views
CREATE VIEW v_todo AS SELECT i.product_id,i.image_id,i.src,i.alt_shop FROM inventory i LEFT JOIN jobs j ON j.product_id=i.product_id AND j.image_id=i.image_id WHERE j.product_id IS NULL OR j.status IN ('error','dry_run');
CREATE VIEW v_changed AS SELECT i.product_id,i.image_id,i.alt_shop AS alt_now,j.alt_new FROM inventory i JOIN jobs j ON j.product_id=i.product_id AND j.image_id=i.image_id WHERE j.status='updated' AND i.alt_shop != j.alt_new;
Tips for Stability and Quality
Safety for character length: clamp_len keeps to 125 characters, only adjust ALT_MAX if your accessibility guidelines allow it.
Language per market: Use different .env files per shop locale, for example ALT_LANG=en for the UK shop.
Cost control: Log tokens per request, optionally in a second table costs, to see AI costs per batch.
CREATE TABLE IF NOT EXISTS costs( ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP, product_id INTEGER, image_id INTEGER, prompt_tokens INTEGER, completion_tokens INTEGER );
Optionally analyze image content: If you want real image descriptions instead of metadata, download the image locally, give the AI a small caption suggestion and combine with product facts. This increases quality, but is more expensive. Metadata-only is usually sufficient and low risk.
Monitoring: Generate a short report, for example number processed, number of errors, top error reasons, average token costs.
Mini Checklist for Go-Live
.env correct, token tested
Run once with DRY_RUN=true, check results
ONLY_EMPTY=true, so as not to overwrite existing good ALT texts
Small batches, watch logs
After the run, spot check in the frontend, then monitor the Images tab in Google Search Console
Comparison: Simple Setup vs. Advanced Setup for ALT Texts in Shopify
| Criterion | Simple Setup | Advanced Setup |
|---|---|---|
| Target group | Small shops with < 200 images | Large shops with several thousand images |
| Approach | Enter ALT texts manually in the Shopify backend or adjust via CSV import | Fully automatic generation & entry via AI + Shopify API |
| Technology | No code necessary | Python script, API access, database for jobs & logging |
| AI usage | Generate texts individually in ChatGPT and copy | AI generates thousands of ALT texts in batches with resumption |
| Time required | High – each image is maintained manually | Very low – even 10,000 images can be processed in a few hours |
| Flexibility | Hardly any: changes must be made manually | High: rules, prompts, languages, character length freely definable |
| Risks | Error rate due to copy-paste, slightly inconsistent texts | Technical setup requires some know-how, but is consistent and scalable |
| Costs | No app costs, only time investment | API costs (OpenAI) + one-time setup effort |
The Python Script
Example Python script; you will find the complete explanation soon in my members area.
import os, time, json, re, sys, csv, sqlite3, math
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass
from urllib.parse import urlparse
import requests
from dotenv import load_dotenv
# ========= ENV =========
load_dotenv()
SHOPIFY_SHOP = os.getenv("SHOPIFY_SHOP") # shop.myshopify.com
SHOP_TOKEN = os.getenv("SHOPIFY_TOKEN") # Admin API Access Token
API_VERSION = os.getenv("API_VERSION", "2025-07")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
LANG = os.getenv("ALT_LANG", "de")
ALT_MAX = int(os.getenv("ALT_MAX", "125"))
BATCH_SIZE = int(os.getenv("BATCH_SIZE", "50"))
DRY_RUN = os.getenv("DRY_RUN", "true").lower() == "true"
ONLY_EMPTY = os.getenv("ONLY_EMPTY", "true").lower() == "true" # only update images without ALT
INCLUDE_VENDOR = os.getenv("INCLUDE_VENDOR", "true").lower() == "true"
RESUME_DB = os.getenv("DB_PATH", "alt_text_jobs.db")
# Backoff for API calls
OAI_MIN_INTERVAL = float(os.getenv("OAI_MIN_INTERVAL", "1.2")) # seconds between OpenAI calls
SHOP_MAX_RETRY = int(os.getenv("SHOP_MAX_RETRY", "3"))
OAI_MAX_RETRY = int(os.getenv("OAI_MAX_RETRY", "3"))
# ========= OpenAI minimal Client =========
OAI_URL = "https://api.openai.com/v1/chat/completions"
OAI_HEADERS = {
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json",
}
# ========= Shopify REST =========
SHOP_BASE = f"https://{SHOPIFY_SHOP}/admin/api/{API_VERSION}"
SHOP_HEADERS = {
"X-Shopify-Access-Token": SHOP_TOKEN,
"Content-Type": "application/json",
}
@dataclass
class ImageRef:
product_id: int
image_id: int
src: str
alt: str
product_title: str
vendor: Optional[str]
tags: Optional[str]
variants: List[Dict[str, Any]]
# ========= Persistence =========
def db_init(path: str):
conn = sqlite3.connect(path)
conn.execute("""
CREATE TABLE IF NOT EXISTS jobs(
product_id INTEGER,
image_id INTEGER,
src TEXT,
alt_old TEXT,
alt_new TEXT,
status TEXT,
error TEXT,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(product_id, image_id)
);
""")
return conn
def db_upsert(conn, ref: ImageRef, alt_new: Optional[str], status: str, error: Optional[str]):
conn.execute("""
INSERT INTO jobs(product_id,image_id,src,alt_old,alt_new,status,error)
VALUES(?,?,?,?,?,?,?)
ON CONFLICT(product_id,image_id) DO UPDATE SET
alt_old=excluded.alt_old,
alt_new=excluded.alt_new,
status=excluded.status,
error=excluded.error,
ts=CURRENT_TIMESTAMP
""", (ref.product_id, ref.image_id, ref.src, ref.alt, alt_new or "", status, error or ""))
conn.commit()
# ========= Utils =========
def clean_text(s: str) -> str:
s = re.sub(r"\s+", " ", s or "").strip()
return s
def clamp_len(s: str, n: int) -> str:
s = s.strip()
return s if len(s) <= n else s[:n].rstrip()
def is_supported_image(src: str) -> bool:
# optional filter if you want to skip incompatible formats
ext = urlparse(src).path.lower().rsplit(".", 1)[-1] if "." in urlparse(src).path else ""
return ext in {"png", "jpg", "jpeg", "webp", "gif"}
def log(*a):
print(*a, file=sys.stdout, flush=True)
# ========= Shopify: Fetch products and images =========
def shopify_get(url: str, params: Dict[str, Any] = None, retry=SHOP_MAX_RETRY) -> Dict[str, Any]:
for i in range(retry):
r = requests.get(url, headers=SHOP_HEADERS, params=params, timeout=60)
if 200 <= r.status_code < 300:
return r.json()
time.sleep(1.5 * (i + 1))
raise RuntimeError(f"Shopify GET failed {r.status_code} {r.text[:300]}")
def shopify_put(url: str, payload: Dict[str, Any], retry=SHOP_MAX_RETRY) -> Dict[str, Any]:
for i in range(retry):
r = requests.put(url, headers=SHOP_HEADERS, json=payload, timeout=60)
if 200 <= r.status_code < 300:
return r.json()
time.sleep(1.5 * (i + 1))
raise RuntimeError(f"Shopify PUT failed {r.status_code} {r.text[:300]}")
def iter_products(limit=250):
page = 1
while True:
url = f"{SHOP_BASE}/products.json"
data = shopify_get(url, {"limit": limit, "page": page})
products = data.get("products", [])
if not products:
break
for p in products:
yield p
page += 1
def collect_images() -> List[ImageRef]:
refs: List[ImageRef] = []
for p in iter_products():
pid = p["id"]
title = p.get("title") or ""
vendor = p.get("vendor")
tags = p.get("tags")
variants = p.get("variants", [])
for img in p.get("images", []):
ref = ImageRef(
product_id=pid,
image_id=img["id"],
src=img.get("src") or "",
alt=img.get("alt") or "",
product_title=title,
vendor=vendor,
tags=tags,
variants=variants
)
refs.append(ref)
return refs
# ========= Prompting =========
def build_context(ref: ImageRef) -> Dict[str, Any]:
colors = []
materials = []
for v in ref.variants:
for i in range(1, 4):
o = v.get(f"option{i}")
if not o:
continue
o_norm = o.lower()
if any(k in o_norm for k in [""]):
colors.append(o.strip())
if any(k in o_norm for k in [""]):
materials.append(o.strip())
ctx = {
"product_title": clean_text(ref.product_title),
"vendor": clean_text(ref.vendor or ""),
"colors": sorted(set([c for c in colors if c])),
"materials": sorted(set([m for m in materials if m])),
"tags": clean_text(ref.tags or "")
}
return ctx
def prompt_alt(ref: ImageRef) -> str:
ctx = build_context(ref)
vendor_part = f"{ctx['vendor']} " if INCLUDE_VENDOR and ctx["vendor"] else ""
instr = f"""
You write concise ALT texts in {LANG}.
Rules:
1. Maximum {ALT_MAX} characters
2. Descriptive and specific
3. No filler phrases
4. Mention material and color if appropriate
5. No hashtags
6. Do not repeat the word image
Return only JSON with the field alt.
Conclusion
Instead of relying on half-baked app solutions, you can use your own Python AI automation to create ALT texts in Shopify in a scalable, reliable, and SEO-oriented way. This not only saves time but also improves the accessibility and visibility of your products. If you need support, feel free to schedule an appointment here.
E-commerce SEO Case Study for PURELEI
E-commerce SEO Case Study: PURELEI.com +100% visibility in 8 months
Case study, SEO, strategies
E-commerce SEO Case Study for PURELEI
E-commerce SEO Case Study: PURELEI.com +100% visibility in 8 months [...]
Case study, SEO
Case Study: How We Generated Over €1.2 Million in Revenue with Targeted Blog Content
Brief overview
A fast-growing brand was able to generate over €1.2 million in revenue within 12 months through targeted content strategies and [...]
Automation
Create Apify Account
Tutorial: Create & Start Apify Account
Step 1: Go to apify.com
Open [...]
Analysis, AI, SEO, SEO Tools
AI Prompt Keyword Mapper
0 (0) How to analyze prompts automatically
Nowadays, prompts – that is, [...]
AI, AI Tools, SEO Tools
ChatGPT German: Use Chat GPT for Free Without Registration
ChatGPT, the advanced language model from OpenAI, is revolutionizing the way [...]
SEO, Shopify
Part 2: Automating Shopify Redirects: Connecting Python Sitemaps, Excel & Matrixify
0 (0) 📖 Part of a series: Why missing redirects cost you traffic [...]
Use my SEO roadmap to get on page 1 of Google!
Sign up for my newsletter and get access to free guides, checklists, and tools.