CSV import preflight: a small read-only Python sample

Runnable Python CSV audit sample, fictional input and four tests. AI-created; source files stay unchanged.

CSV import preflight: a small read-only Python sample

A CSV can parse successfully and still fail an import: repeated IDs, the wrong number of fields, or a required value missing. This standalone Python 3.10+ sample reports those conditions without rewriting the source. It uses only the standard library.

This is an original, AI-created demonstration from MissionMoney Data Lab, a new human-owned service. It is not a customer project. Four automated tests passed locally, covering quoted multiline records and UTF-8 BOMs, known import errors, duplicate headers, and the issue-sample limit.

Save the following as csv_audit.py:

"""Read-only CSV import preflight. Python 3.10+, standard library only."""
from __future__ import annotations

import argparse
import csv
import hashlib
import json
from collections import Counter
from pathlib import Path


def audit(path: Path, *, key: str | None = None, required=(), delimiter=','):
    """Keep source bytes unchanged; flag ambiguous records instead of guessing."""
    if len(delimiter) != 1:
        raise ValueError('Delimiter must be one character')
    digest = hashlib.sha256()
    with path.open('rb') as source:
        for block in iter(lambda: source.read(1024 * 1024), b''):
            digest.update(block)
    issues = []
    counts = Counter()

    def flag(code, line, detail):
        counts[code] += 1
        if len(issues) < 100:
            issues.append({'code': code, 'line': line, 'detail': detail})

    rows = 0
    keys = {}
    headers = []
    with path.open('r', encoding='utf-8-sig', newline='') as source:
        reader = csv.reader(source, delimiter=delimiter, strict=True)
        headers = next(reader, [])
        if not headers:
            flag('missing_header', 1, 'No header row')
        for name, count in Counter(headers).items():
            if count > 1:
                flag('duplicate_header', 1, name)
        if any(not name.strip() for name in headers):
            flag('blank_header', 1, 'At least one column has no name')
        requested = set(required) | ({key} if key else set())
        for name in sorted(requested - set(headers)):
            flag('missing_column', 1, name)
        required_indexes = [(name, headers.index(name)) for name in required if headers.count(name) == 1]
        key_index = headers.index(key) if key and headers.count(key) == 1 else None
        for row in reader:
            rows += 1
            line = reader.line_num
            if len(row) != len(headers):
                flag('column_count', line, f'Expected {len(headers)}, got {len(row)}')
                continue
            for name, index in required_indexes:
                if not row[index].strip():
                    flag('missing_value', line, name)
            if key_index is not None:
                value = row[key_index]
                if value:
                    if value in keys:
                        flag('duplicate_key', line, f'{key}: same value as line {keys[value]}')
                    else:
                        keys[value] = line
            for index, value in enumerate(row):
                if value != value.strip():
                    flag('surrounding_whitespace', line, headers[index])
                if value.lstrip().startswith(('=', '+', '-', '@')):
                    flag('formula_like_value', line, headers[index])
    return {
        'source': path.name,
        'sha256': digest.hexdigest(),
        'rows': rows,
        'columns': headers,
        'issue_counts': dict(counts),
        'issue_samples': issues,
        'samples_truncated': sum(counts.values()) > len(issues),
        'source_modified': False,
        'notes': [
            'Keys use exact string equality; no case or whitespace normalization is inferred.',
            'Formula-like values are review flags; legitimate negative numbers can be flagged.',
            'Line numbers refer to the ending physical line of each CSV record.',
            'Only structure and selected fields are checked; this is not full data validation.'
        ]
    }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('input', type=Path)
    parser.add_argument('--key')
    parser.add_argument('--required', default='')
    parser.add_argument('--delimiter', default=',')
    args = parser.parse_args()
    try:
        result = audit(args.input, key=args.key,
                       required=[x.strip() for x in args.required.split(',') if x.strip()],
                       delimiter=args.delimiter)
    except (OSError, ValueError, csv.Error) as error:
        parser.exit(2, f'Cannot audit input: {error}\n')
    print(json.dumps(result, ensure_ascii=True, indent=2))


if __name__ == '__main__':
    main()

Try it on fictional data

Save this as demo-input.csv. The repeated ID and malformed fields are intentional; the email domain is reserved for examples.

id,email,total
001, alice@example.invalid ,19.95
001,,19.95
002,bob@example.invalid,29.90,extra
003,carol@example.invalid,=1+1

Run:

python csv_audit.py demo-input.csv --key id --required id,email

The report contains four data rows and one instance of each of these flags: surrounding whitespace, missing required value, duplicate key, wrong column count, and a formula-like value. ID matching uses exact strings, so leading zeros are preserved. A legitimate negative number also receives the formula-like flag: review it rather than treating it as malicious or changing it automatically.

Reproduce the tests

Save the following as tests/test_csv_audit.py next to the script, then run python -m unittest discover -s tests -v.

import tempfile
import unittest
from pathlib import Path
from csv_audit import audit


class CsvAuditTests(unittest.TestCase):
    def check(self, data, **kwargs):
        with tempfile.TemporaryDirectory() as directory:
            path = Path(directory) / 'source.csv'
            path.write_bytes(data)
            result = audit(path, **kwargs)
            self.assertEqual(path.read_bytes(), data)
            return result

    def test_quotes_multiline_bom_and_leading_zero_ids(self):
        result = self.check(b'\xef\xbb\xbfid,note\r\n001,"two, parts"\r\n002,"line\r\nbreak"\r\n', key='id', required=['id'])
        self.assertEqual(result['rows'], 2)
        self.assertEqual(result['issue_counts'], {})

    def test_specific_import_problems(self):
        result = self.check(b'id,email\n001, a@example.invalid \n001,\n002,x,extra\n003,=1+1\n', key='id', required=['id','email'])
        self.assertEqual(result['issue_counts'], {'surrounding_whitespace':1,'missing_value':1,'duplicate_key':1,'column_count':1,'formula_like_value':1})

    def test_duplicate_headers_do_not_silently_pick_key(self):
        result = self.check(b'id,id\n1,2\n1,3\n', key='id', required=['email'])
        self.assertEqual(result['issue_counts'], {'duplicate_header':1,'missing_column':1})

    def test_large_issue_list_is_bounded(self):
        result = self.check(b'id\n' + b'1\n' * 150, key='id')
        self.assertEqual(result['issue_counts']['duplicate_key'],149)
        self.assertEqual(len(result['issue_samples']),100)
        self.assertTrue(result['samples_truncated'])


if __name__ == '__main__':
    unittest.main()

Limits: UTF-8 input only, no full business-rule validation, no semantic deduplication, no repairs. The issue sample is capped at 100 entries, while all issue counts are retained. Duplicate-key tracking uses memory proportional to the number of distinct keys. Run it against a stable input file; concurrent changes during its two read passes are outside this demo.

For a specific Python or PowerShell repair, the pilot service covers one agreed issue for 30,000 sats: https://njump.me/naddr1qvzqqqrkcgpzq970deh37n4tj002wwlcxtsr2l56htf4hv5cav7gjdgd27vm5zvzqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hszrthwden5te0dehhxtnvdakqq8ts096xsmmw94cx7am9wfeksetvdskkgct5vykhyetsv95hy5385nw. Reply with a redacted reproduction before ordering.


Write a comment