Bobobk

Practical Python, Linux and automation guides

Diagnose CSV Delimiters with Python: Sniffer, Explicit Formats, and Full Validation

2026-10-31 Technology
Diagnose CSV Delimiters with Python: Sniffer, Explicit Formats, and Full Validation
On this page

A CSV export turns into one column, or a comma inside a note creates extra fields. The file may use semicolons or tabs rather than being damaged. This beginner-to-intermediate tutorial builds a read-only delimiter inspector with Python’s standard library: suggest a candidate, then check the header and field counts throughout the file.

1. Method: suggest first, then validate

  1. When the export format is known, pass --delimiter semicolon or tab rather than guessing again.
  2. Otherwise, suggest a delimiter from comma, semicolon, tab, and pipe only.
  3. Read the entire file against an expected header order and require matching field counts. Print successful JSON only after every record passes.

Python’s official Sniffer documentation describes deriving a dialect from sample text and restricting candidate delimiters. A suggestion is not proof: a short sample, a cut-off quoted field, or irregular input can defeat inference or produce the wrong choice.

We do not infer header presence or adopt every inferred dialect setting; only the delimiter is taken. Our contract requires a header, double-quoted fields, doubled embedded quotes, and no custom escape character. This complements CSV header and structure validation: diagnose the delimiter, then fix the data contract.

2. Setup: create an isolated demo directory

Use an existing Python 3.9 or newer interpreter, with no third-party packages. Commands work in Linux bash/zsh and were tested with Python 3.11.4. Use a new directory name when repeating the experiment to avoid overwriting previous files.

  1. Run in one terminal:
python3 --version
mkdir csv-delimiter-demo
cd csv-delimiter-demo
python3 - <<'PY_DATA'
from pathlib import Path
Path("samples.csv").write_bytes(
    b'\xef\xbb\xbfid;note\n001;"alpha,beta"\n002;"two\nlines"\n'
)
PY_DATA

The semicolon-separated file has a UTF-8 BOM, a comma in the first note, and a two-line second note. There are only two data records.

  1. Save the next section as inspect_csv.py in the current directory, or download the complete script.

3. Complete implementation: rewind and check the entire file

"""Inspect completed local CSV files; guess only the delimiter. Python 3.9+."""
import argparse
import csv
import json
from pathlib import Path
import sys

DELIMITERS = {"comma": ",", "semicolon": ";", "tab": "\t", "pipe": "|"}


def inspect_csv(path, mode, columns):
    if len(columns) < 2 or len(set(columns)) != len(columns):
        raise ValueError("expected columns must be unique and number at least two")
    if any(not name.strip() for name in columns):
        raise ValueError("expected column names must not be blank")
    with path.open(encoding="utf-8-sig", newline="") as stream:
        if mode == "auto":
            sample = stream.read(8192)
            try:
                delimiter = csv.Sniffer().sniff(sample, delimiters=",;\t|").delimiter
            except csv.Error as exc:
                raise ValueError("cannot suggest delimiter; specify --delimiter") from exc
            stream.seek(0)
        else:
            delimiter = DELIMITERS[mode]
        reader = csv.reader(stream, delimiter=delimiter, quotechar='"',
                            doublequote=True, skipinitialspace=False, strict=True)
        try:
            header = next(reader, None)
            if header != columns:
                raise ValueError(f"header mismatch: expected {columns!r}, got {header!r}")
            records = 0
            for row in reader:
                if len(row) != len(columns):
                    raise ValueError(
                        f"record {records + 1}, ending line {reader.line_num}: "
                        f"expected {len(columns)} fields, got {len(row)}"
                    )
                records += 1
        except csv.Error as exc:
            raise ValueError(f"CSV error near physical line {reader.line_num}: {exc}") from exc
    return {"delimiter": delimiter, "columns": columns, "records": records,
            "physical_lines": reader.line_num, "selection": mode}


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("file", type=Path)
    parser.add_argument("--delimiter", choices=["auto", *DELIMITERS], default="auto")
    parser.add_argument("--columns", nargs="+", required=True)
    args = parser.parse_args()
    try:
        report = inspect_csv(args.file, args.delimiter, args.columns)
    except (OSError, UnicodeError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(json.dumps(report, ensure_ascii=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Sampling reads at most 8192 characters, then seek(0) returns to the start; forgetting to rewind skips the header. utf-8-sig accepts UTF-8 with or without an initial BOM, not arbitrary encodings. For decoding failures, establish the source encoding with the CSV-to-UTF-8 tutorial.

The official csv.reader documentation specifies newline="" for file objects, while the line_num documentation distinguishes physical lines from records. Let the parser handle quoted newlines instead of splitting physical lines yourself.

Use this tool for small, completed local regular files that remain unchanged during inspection, not pipes, devices, or actively appended files. Full validation iterates over records without retaining them all, but an oversized individual field is still subject to parser limits. This is not resource isolation for huge untrusted inputs.

4. Usage: automatic suggestions and explicit formats

  1. Show help, then try automatic selection and an explicit semicolon:
python3 inspect_csv.py --help
python3 inspect_csv.py samples.csv --columns id note
python3 inspect_csv.py samples.csv --delimiter semicolon --columns id note

Expected automatic-mode output:

{"delimiter": ";", "columns": ["id", "note"], "records": 2, "physical_lines": 4, "selection": "auto"}

Explicit mode produces the same counts with selection set to semicolon. physical_lines is 4 but records is 2 because one note spans two lines. 001 remains a string rather than becoming a number.

  1. Deliberately use the wrong delimiter and inspect the exit status:
python3 inspect_csv.py samples.csv --delimiter comma --columns id note
printf 'exit=%s\n' "$?"

The error below goes to stderr, followed by the shell’s exit=1:

ERROR: header mismatch: expected ['id', 'note'], got ['id;note']
Exit code Meaning
0 All records match the expected header and field counts; stdout contains one JSON report.
1 File, decoding, inference, or structural failure; stderr contains an error and stdout is empty.
2 Invalid CLI arguments, such as an unsupported delimiter name.
  1. Once the upstream format is confirmed, pin it in automation and use && to stop after failure:
python3 inspect_csv.py samples.csv --delimiter semicolon --columns id note \
  && printf 'Input structure accepted\n'

The tool neither rewrites input nor prints data records, but error messages may include the actual header. Do not send sensitive headers into public logs. Passing these checks does not validate field values, unique IDs, or business rules.

5. Validation: test success and failure cases

Save this as test_inspect.py, or download the test script, then run python3 test_inspect.py in the demo directory:

"""Run from the directory containing inspect_csv.py. No third-party packages."""
import csv
import hashlib
import io
from pathlib import Path
import subprocess
import sys
import tempfile

script = Path("inspect_csv.py").resolve()
cases = [
    ("semicolon", b'id;note\n001;"alpha,beta"\n', "auto", 0, ";"),
    ("comma", b'id,note\n001,"alpha,beta"\n', "auto", 0, ","),
    ("tab", b'id\tnote\n001\talpha\n', "tab", 0, "\t"),
    ("pipe", b'id|note\n001|alpha\n', "pipe", 0, "|"),
    ("multiline", b'\xef\xbb\xbfid;note\r\n001;"a\nb"\r\n', "semicolon", 0, ";"),
    ("wrong", b'id;note\n001;alpha\n', "comma", 1, None),
    ("ragged", b'id;note\n001;alpha;extra\n', "semicolon", 1, None),
    ("blank", b'id;note\n\n', "semicolon", 1, None),
    ("quotes", b'id;note\n001;"unfinished\n', "semicolon", 1, None),
    ("encoding", b'id;note\n001;\xff\n', "semicolon", 1, None),
    ("empty", b'', "auto", 1, None),
]
with tempfile.TemporaryDirectory() as directory:
    for name, data, mode, expected, delimiter in cases:
        path = Path(directory) / (name + ".csv")
        path.write_bytes(data)
        before = hashlib.sha256(data).hexdigest()
        result = subprocess.run(
            [sys.executable, str(script), str(path), "--delimiter", mode,
             "--columns", "id", "note"], capture_output=True, text=True
        )
        assert result.returncode == expected, (name, result.stderr)
        assert hashlib.sha256(path.read_bytes()).hexdigest() == before
        if expected == 0:
            import json
            report = json.loads(result.stdout)
            assert report["delimiter"] == delimiter
            assert report["records"] == 1 and not result.stderr
        else:
            assert not result.stdout and result.stderr.startswith("ERROR:")
        print(f"PASS {name}: exit={expected}")
# A naive split breaks the quoted comma; csv.reader preserves it.
assert '001,"alpha,beta"'.split(",") != ["001", "alpha,beta"]
assert next(csv.reader(io.StringIO('001,"alpha,beta"'))) == ["001", "alpha,beta"]
print("PASS quoted comma: field values preserved")

Expect 11 passing case messages followed by PASS quoted comma: field values preserved. Tests check exit codes, output streams, and unchanged input hashes, covering BOM, CRLF, embedded newlines, wrong delimiters, ragged rows, blank records, unclosed quotes, invalid UTF-8, and empty input.

6. Troubleshooting: do not repair CSV by replacing characters

Symptom Concrete action
cannot suggest delimiter Confirm nonempty input, establish the export format, then use --delimiter. Do not silently fall back to comma. On Python 3.11.4, a CRLF file with a multiline quoted field also defeated inference in our test, but parsed correctly with an explicit delimiter.
header mismatch Check delimiter, header order, case, and spaces. Column-name whitespace is not stripped.
expected 2 fields, got ... Inspect the ending physical line for missing fields, extra delimiters, or blank records; fix the export.
CSV error Check closing quotes and doubled embedded quotes. strict=True is not business validation.
field larger than field limit First rule out a wrong delimiter collapsing a whole line into one field, then follow CSV field-size troubleshooting.

Do not convert with replace(";", ",") or read with split(","): neither understands quoted delimiters. For a real conversion, confirm the input format first, then use csv.reader and csv.writer to write a new output path.

7. Summary

Use a sample to narrow the investigation, then validate the candidate against a known header and every record’s field count. After confirming the format, pin the delimiter and let exit codes gate downstream work instead of treating a guess as a permanent data contract.

Tags

1024 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print probability prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sql sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode upsert urllib usdt utf-8 uv venv vhhbert vite wallet web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 迅雷解析

友情链接