How to Handle Bad CSV Data in Python (Without Crashing)
Real vendor CSV files are messy: some rows use semicolons instead of commas, prices have currency symbols, and blank rows sneak in. A naive importer dies on the first bad row and loses all the good data after it. Wrap per-row parsing in try/except so one bad row gets skipped and logged while the rest import cleanly.
Why CSV imports crash halfway through
Python's csv.DictReader reads row by row, but your code crashes when it tries to
process a bad row - a missing field comes back as None, a price like $12.99 or
EUR 12,99 raises ValueError on float(), and a blank row has no data at all.
Without any guard, the whole import dies at that row and nothing after it reaches
the database.
Wrap per-row parsing in try/except
The key pattern: validate and parse first (no database state changes), then write. If parsing fails, log and continue.
import re, csv, psycopg2
def clean_price(raw):
# strip currency symbols and whitespace, then parse
s = re.sub(r'[^\d.\-]', '', str(raw or ''))
return float(s)
def import_products(csv_path, conn):
cur = conn.cursor()
imported = skipped = 0
with open(csv_path, encoding='utf-8', errors='replace') as f:
reader = csv.DictReader(f)
for row in reader:
try:
sku = (row.get('sku') or '').strip()
name = (row.get('name') or '').strip()
category = (row.get('category') or '').strip()
if not sku or not name or not category:
skipped += 1
continue
price = clean_price(row.get('price'))
stock = int(row.get('stock') or 0)
if price <= 0:
skipped += 1
continue
except (ValueError, TypeError) as e:
print(f"Skipped row {row.get('sku', '?')}: {e}")
skipped += 1
continue
cur.execute(
"INSERT INTO products (sku, name, category, price, stock) "
"VALUES (%s, %s, %s, %s, %s) "
"ON CONFLICT (sku) DO UPDATE SET "
"name=EXCLUDED.name, price=EXCLUDED.price, stock=EXCLUDED.stock",
(sku, name, category, price, stock)
)
imported += 1
conn.commit()
cur.close()
print(f"Imported: {imported} Skipped: {skipped}")
What each guard does
errors='replace'onopen()- bad encoding bytes become?instead of raisingUnicodeDecodeError. Your loop never sees the encoding problem.row.get('field') or ''+.strip()- a missing column key or an empty field both collapse to an empty string; thenot skucheck catches it.clean_priceregex - strips$,EUR, spaces, commas beforefloat(). Extend the regex for other formats as your vendors evolve.try/except (ValueError, TypeError)- catches unparseable numbers andNonearithmetic in one block, prints which SKU caused the skip, and continues.- Validate before any DB write - if parsing raises, you haven't touched the database, so there's nothing to roll back. Keeps the error path simple.
Adding a dead-letter log
For production pipelines, write rejected rows to a separate file instead of just printing them:
with open('import_errors.csv', 'w', newline='') as errfile:
writer = csv.DictWriter(errfile, fieldnames=['row', 'reason'])
writer.writeheader()
# inside the except block:
writer.writerow({'row': str(row), 'reason': str(e)})
Teams review this file to find patterns - a vendor that always sends prices with commas as decimal separators, or a category field that went blank after an upstream schema change.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Wrapping per-row CSV parsing in try/except to skip bad rows and log the reason
- Cleaning dirty price fields with a regex before float() conversion
- Writing rejected rows to a dead-letter CSV for later review
FAQ
How do I skip bad rows in a Python CSV import without crashing?
Wrap the per-row parsing logic in a try/except block. Validate and parse all fields first - before any database write - then on ValueError or TypeError, print the row identifier and the error, increment a skip counter, and call continue to move to the next row.
How do I parse a price that has currency symbols like $ or EUR in Python?
Use re.sub to strip all characters except digits, periods, and hyphens before calling float(). For example: re.sub(r'[^\d.\-]', '', str(raw)) removes $, EUR, commas, and whitespace, leaving a parseable number string.
Why does my CSV import crash even when most rows are valid?
Without per-row error handling, one bad row raises an unhandled exception that stops the entire loop. All valid rows after that point are lost. Wrapping parsing in try/except per row means only the bad row is skipped - the import continues to completion.
How do I skip bad lines when reading a CSV with pandas?
Pass on_bad_lines to read_csv: pd.read_csv("file.csv", on_bad_lines="skip") drops malformed rows instead of raising (on older pandas, use error_bad_lines=False). For dirty values within otherwise-valid rows, read everything as strings and clean per-column afterward rather than failing the whole read.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →