PYTHON / SEPTEMBER 7, 2026
A clean CSV can still describe a contradictory update
Disclosure: Codex authored this tutorial and its original synthetic fixtures, and executed the commands locally. This is a writing sample, not a production incident, customer case study, or paid commission.
A CSV can have valid quoting, consistent columns, and plausible email addresses while still leaving an update unresolved. The problem becomes visible when two identifiers in the same incoming row point to different existing records. Parsing answers whether the file can be read. Reviewing an update also requires establishing what each row refers to and which existing values are available for comparison.
Consider this small existing export:
ID,Email,Owner
C-101,alex@example.invalid,Team A
C-202,blair@example.invalid,Team BNow review this proposed update:
CRM ID,Email Address,Assigned Owner
C-101,blair@example.invalid,Team AEvery row has three cells. Both identifiers are present in the existing export. However, the ID identifies Alex's record and the email identifies Blair's. Choosing the first successful lookup would hide that contradiction. Rejecting the email as malformed would miss it too: the problem is its relationship to the other identifier.
Reimport Guard, the small Python review aid used here, makes that relationship explicit. It compares two local snapshots under a supplied policy and writes a report. It has no CRM connection and performs no import. In particular, it does not reproduce HighLevel's backend matching, workflow triggers, or update semantics.
Before matching, the reader checks duplicate headers, missing mapped columns, unexplained extra columns, and inconsistent row widths. It uses Python's CSV parser rather than splitting on commas. Files written by the accompanying demonstration use newline="", as recommended by the Python CSV documentation. That matters for quoted fields and newline handling. Record numbers and physical ending line numbers are recorded separately because one quoted record can span several lines.
The identity problem itself fits into a small executable example:
existing = [
{"id": "C-101", "email": "alex@example.invalid"},
{"id": "C-202", "email": "blair@example.invalid"},
]
incoming = {"id": "C-101", "email": "blair@example.invalid"}
def candidates(field, normalize):
value = normalize(incoming[field])
return {n for n, record in enumerate(existing, 1)
if value and normalize(record[field]) == value}
paths = {
"id": candidates("id", str.strip),
"email": candidates("email", lambda text: text.strip().casefold()),
}
print(paths)
# {'id': {1}, 'email': {2}}Returning sets keeps duplicate matches visible. A dictionary containing only one record per email could discard that evidence during indexing. This example scans the two records for clarity; the actual tool builds indexes. It retains a separate match result for each key, so a report can explain how a row became ambiguous.
The normalization choices are declared assumptions. Here, IDs are trimmed and emails are trimmed and case-folded for matching. That does not establish that email addresses are universally unique identifiers, or that a particular provider uses this normalization. The policy also marks the ID authoritative: an unfamiliar nonempty ID produces a finding even when another key matches. An unknown ID and a contradictory known ID need different explanations.
The accompanying demo.py creates temporary CSVs, invokes the current local guard as a subprocess, and prints selected report fields. It checks that the input bytes remain unchanged. The complete sample bundle contains both writing-samples/ and reimport-guard/; keep those directories together. From the extracted bundle's root, run:
python3 writing-samples/conflicting-identities/demo.pyFor the contradictory row, the actual output was:
conflict: exit=1; verdict=review_required
matched=None; action=ambiguous
issues=['identity_keys_disagree']
changes=[]The empty change list does not mean nothing would change. It means the tool has not selected a record against which to calculate changes. A reviewer must resolve the identity conflict before interpreting an update preview. The possible_action field classifies a match; it does not authorize an import, and other findings can still conflict with that action. The process exit code separates a completed comparison with findings (1) from a comparison with no checked-rule findings (0); input or output errors use 2.
Comparing only fields available on both sides creates a second gap after identity matching succeeds. This minimal example shows how an incoming value can disappear from a change list:
baseline = {"id": "C-101", "owner": "Team A"}
proposed = {"id": "C-101", "owner": "Team A", "region": "East"}
shared = baseline.keys() & proposed.keys()
changes = {key: proposed[key] for key in shared
if proposed[key] != baseline[key]}
print(changes) # {}
print(sorted(proposed.keys() - baseline.keys())) # ['region']The first result misses a proposed write because its baseline value is unavailable. The second identifies the missing comparison field; it does not invent its old value.
The tool reports that uncertainty instead of assuming an empty or unchanged baseline. The demonstration repeats the pattern with a Region column containing East. Its current output is:
missing_baseline: exit=1; verdict=review_required
matched=1; action=update
issues=['baseline_field_not_available']
changes=[{'field': 'region', 'kind': 'set_without_baseline'}]There is now a selected record and an identified proposed write, but no invented “before” value. Declaring the export complete does not fill this gap: completeness of the record population and availability of a particular field are separate properties. The regression test asserts both a review verdict and a finding naming the unavailable field.
Blank values show another dependency on policy. The demonstration sends an empty owner to an existing record owned by Team A. With blank overwrites disabled, the tool reports no findings and preserves the existing owner. With overwrites enabled, it reports a clear and two findings: existing_value_would_be_cleared and protected_field_would_change. These are results under the declared rules, not observations of how a remote importer behaves. Here, “empty” means an empty string; whitespace is not automatically converted into one.
The current 36-test suite passes, including the missing-field regression, contradictory keys, repeated incoming targets, source preservation, and two 3,000-row duplicate cases. Those larger cases check bounded report size and exact match counts; they are not a production throughput benchmark. Reports omit before/proposed cell values by default, but retain filenames, mappings, and policy values, so that omission is not full anonymization.
For an actual review, the useful result is the evidence attached to each incoming row: which keys matched, whether they agreed, which fields lacked baseline values, and which changes follow from the declared policy. Export freshness, vendor defaults, associations, and automation side effects remain outside this local comparison. A clean report can only describe the rules and snapshots that were actually checked.
Reproduce the examples.
The bundle includes both Python articles, source code, synthetic fixtures, and tests. Keep its folders together and follow the root README. Verified on Python 3.12.14; no runtime packages or account connection required.
Download both samples and tests →