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.

Data Engineerpythoncsvdata-cleaning

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

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

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

Make an ETL Pipeline IdempotentData projectSpeed Up a Slow SQL Report QueryData projectConvert Timestamps to Local Time (Timezones)Data projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

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 →