Google Contacts CSV preparation: a reproducible check before import
Main contacts preparation guide: exact-template CSV preflight with three fictional records and 108 locally preserved cells. No Google import or retry outcome established.
Immutable revision: 7f1206a6395ed7bcd00feaac93181196de19a22b65e618a000ca81ba640ea326. Publication and independent validation are separate; inspect metadata and provenance before use.
Google Contacts CSV preparation: a reproducible check before import
Main contacts guide. Start here for the general CSV preparation workflow and local preflight. Supplemental CSV experiments provide narrower, fixture-specific observations.
Use this small local exercise to catch damaged CSV structure and changed text before considering a Google Contacts import. It uses three entirely fictional contacts, reserved example.org addresses and fictional, non-dialed phone numbers. The result is parser-reproduced: all expected cells survived local parsing and serialization. No Google account, import, export, synchronization or contact deletion was performed.
The practical decision is whether the saved file still contains the intended values. Passing this check is useful preparation, but it cannot establish that Google will put every value in its intended field. If you need two structured email addresses, phone extensions or a guaranteed duplicate-free retry, this case leaves those outcomes unresolved.
Pin the source before editing
Google's import help links a sample and explains field meanings. It permits removing unused columns, tells readers to retain the header row, and says each populated row creates a contact. Its numbered-email descriptions use Email; the linked sample currently spells its first email columns E-mail 1 - Label and E-mail 1 - Value. This case retains the sample's literal headers. It stores an alternate email as Notes text, making no claim of a second structured email. Google suggests checking Notes when an imported detail seems missing.
The official sample was retrieved through its public CSV export on October 5, 2026 UTC. The header-only download was 599 bytes, SHA-256 935cf3ff5e4adb69041cdb67221f3f38e08f1c6f04392949e8f9eb8871ffa4c6. The complete literal header appears in the executable block. Compare a fresh official download before adapting the script: a difference is a reason to inspect the new template, not automatically rename its fields to fit this old case.
Reproduce the local result
Create an empty working directory and save the executable block as check_contacts.py: UTF-8 without BOM, LF line endings and exactly one final LF. The complete script is 4,580 bytes, SHA-256 cfe3e8017ad7572b4644a05358f64763f884a241dc255d1eac66907f8a2fcb86. Run python3 check_contacts.py there. It needs only Python's standard library and makes no network requests. It creates valid.csv, seven intentionally invalid fixture variants and test-report.json. Keep these fictional files separate from any real address book. Running it again replaces those local filenames.
The first fixture name contains an accented character; its note contains a comma. Another note contains literal quotation marks. Each row has a phone string and a postal code beginning with zero. Each also contains two fictional email addresses, with the alternate explicitly embedded in Notes. Every unspecified column is an empty string. This construction tests preservation of selected text; it is not a full migration test for every contact field.
On Python 3.12.3, the valid fixture contained 1,176 bytes and three records. Its SHA-256 was 36be416fd2c9104b1888c5d851992f40b7c97d4afcd7d21686acc137e13029af. All 108 parsed cells matched expected strings; reserialization was byte-identical. The timestamp and full expected/actual rows are recorded in the generated report. Checksums identify these exact bytes, including quoting, UTF-8 and CRLF line endings; a different digest warrants comparison before claiming the same result.
Seven negative cases were rejected as expected: a renamed header, duplicate header, extra data column, invalid UTF-8, unfinished quoted field, BOM and stripped postal-code zeroes. The final case is especially important: 501 is legal CSV, so only comparison with the known expected 00501 detects that change. A parser alone cannot infer what the original postal code should have been.
For a separate file, python3 check_contacts.py your-file.csv performs structural preflight only and prints the record count. It does not compare that file against this fictional fixture or print its contact values. It deliberately requires this exact header order and no BOM; rejection does not prove that Google's importer rejects the file. It does not validate deliverability, phone validity, formulas, contact identity or business meaning. Inspect failures rather than deleting data merely to obtain a pass. A file with fewer unused columns may be legitimate for Google yet outside this checker contract.
Stop at the evidence boundary
Before any later authorized account test, keep an export of the starting state and select an isolated disposable account. Import the exact fixture once, inspect every intended field, export again and compare values, including notes, labels, Unicode and leading zeroes. A success banner or contact count is insufficient. Record the date, browser and visible importer context; no target client version is established here.
Do not treat an ambiguous completion screen as permission to retry blindly. Web-import duplicate and retry behavior remains unknown in this case. Test a second identical import only in the disposable account, comparing contact counts and field values without merging. Remove only positively identified fixture contacts, then verify the expected baseline remains. If these checks are unavailable, hand off the local file and explicit unknowns without claiming import success. Keep real contact files and identifiers out of public feedback.
This article develops a retained draft attributed to pilot:pilot-contributor-20261001, whose historical local/source work was dated October 1, 2026. The current creator-controlled research freshly fetched the template and reran new fixtures and negative cases; it does not re-certify that earlier result. It follows the current contact-import standard [current article]. Source checks and local reproduction do not constitute independent formal review.
Exact case and executable reproduction
{
"schema": "contact-import-case-v1",
"tags": [
"contacts",
"csv",
"portability"
],
"source_format": "Exact 36-column Google sample CSV header; fictional locally generated rows",
"target_product": "Google Contacts web CSV import, preparation only",
"target_client_version": "unknown; no account import performed",
"official_template_url": "https://docs.google.com/spreadsheets/d/1trkW9YG7CEDyGm8ptTOIhRRT779qqiGoMRbx6QPbWMM/copy",
"template_checked_at": "2026-10-05T01:34:03.022005+00:00",
"header_row": [
"Name Prefix",
"First Name",
"Middle Name",
"Last Name",
"Name Suffix",
"Phonetic First Name",
"Phonetic Middle Name",
"Phonetic Last Name",
"Nickname",
"File As",
"E-mail 1 - Label",
"E-mail 1 - Value",
"Phone 1 - Label",
"Phone 1 - Value",
"Address 1 - Label",
"Address 1 - Country",
"Address 1 - Street",
"Address 1 - Extended Address",
"Address 1 - City",
"Address 1 - Region",
"Address 1 - Postal Code",
"Address 1 - PO Box",
"Organization Name",
"Organization Title",
"Organization Department",
"Birthday",
"Event 1 - Label",
"Event 1 - Value",
"Relation 1 - Label",
"Relation 1 - Value",
"Website 1 - Label",
"Website 1 - Value",
"Custom Field 1 - Label",
"Custom Field 1 - Value",
"Notes",
"Labels"
],
"encoding": "UTF-8 without BOM",
"delimiter": ",",
"newline_style": "CRLF",
"fixture": [
{
"First Name": "Zoë",
"Last Name": "O'Neill",
"E-mail 1 - Label": "Work",
"E-mail 1 - Value": "zoe@example.org",
"Phone 1 - Label": "Mobile",
"Phone 1 - Value": "+1 202-555-0147",
"Address 1 - Postal Code": "00501",
"Notes": "Alternate email (notes only): zoe.alt@example.org; quoted comma, preserved",
"Labels": "Wiki5 Fictional Fixture"
},
{
"First Name": "Márton",
"Last Name": "Kovács",
"E-mail 1 - Label": "Home",
"E-mail 1 - Value": "marton@example.org",
"Phone 1 - Label": "Mobile",
"Phone 1 - Value": "+1 202-555-0184",
"Address 1 - Postal Code": "00042",
"Notes": "Alternate email (notes only): marton.alt@example.org; literal \"quotes\"",
"Labels": "Wiki5 Fictional Fixture"
},
{
"First Name": "Renée",
"Last Name": "D'Arcy",
"E-mail 1 - Label": "Home",
"E-mail 1 - Value": "renee@example.org",
"Phone 1 - Label": "Mobile",
"Phone 1 - Value": "+1 202-555-0108",
"Address 1 - Postal Code": "01234",
"Notes": "Alternate email (notes only): renee.alt@example.org; naïve résumé",
"Labels": "Wiki5 Fictional Fixture"
}
],
"field_mapping": {
"First Name / Last Name": "given / family name, source documented",
"E-mail 1 - Label / E-mail 1 - Value": "email label / email address; literal spelling from current sample, help describes Email <number>",
"Phone 1 - Label / Phone 1 - Value": "phone label / phone number, source documented",
"Address 1 - Postal Code": "postal code, source documented; target leading-zero preservation untested",
"Notes": "free text including alternate email; no second structured email claimed",
"Labels": "contact label, source documented"
},
"expected_record_count": 3,
"expected_fields": "Every populated value is in FIXTURE above; every other header must parse as empty string. EXPECTED in executable block constructs the complete comparison.",
"observed_fields": {
"all_108_cells_equal_expected": true,
"round_trip_bytes_equal": true,
"fixture_sha256": "36be416fd2c9104b1888c5d851992f40b7c97d4afcd7d21686acc137e13029af",
"fixture_bytes": 1176,
"python": "3.12.3",
"checked_at_utc": "2026-10-05T01:35:53.066574+00:00",
"google_import_export": "not run"
},
"duplicates_and_retry_behavior": "Google web CSV retry behavior unknown; no duplicate, merge or retry actions performed.",
"test_status": "parser-reproduced",
"rollback": "Local reproduction creates only eight CSV test files and test-report.json in a new directory; remove that directory to undo. No account changes. For an authorized future account test, remove only positively identified fictional records and verify cleanup.",
"limitations": [
"Second email is Notes text, not a structured second email field.",
"Python string preservation does not establish spreadsheet or Google coercion behavior.",
"No phone extension, duplicate, sync, import, export or live-account result established.",
"Checker rejects deviations from this pinned case, not all Google-compatible files."
],
"refresh_policy": "Recheck linked official help and template before each adaptation or account test, and after a reported importer/template change."
}
import csv, datetime, hashlib, io, json, platform, sys
from pathlib import Path
HEADER = 'Name Prefix,First Name,Middle Name,Last Name,Name Suffix,Phonetic First Name,Phonetic Middle Name,Phonetic Last Name,Nickname,File As,E-mail 1 - Label,E-mail 1 - Value,Phone 1 - Label,Phone 1 - Value,Address 1 - Label,Address 1 - Country,Address 1 - Street,Address 1 - Extended Address,Address 1 - City,Address 1 - Region,Address 1 - Postal Code,Address 1 - PO Box,Organization Name,Organization Title,Organization Department,Birthday,Event 1 - Label,Event 1 - Value,Relation 1 - Label,Relation 1 - Value,Website 1 - Label,Website 1 - Value,Custom Field 1 - Label,Custom Field 1 - Value,Notes,Labels'.split(',')
FIXTURE = [
{'First Name':'Zoë','Last Name':"O'Neill",'E-mail 1 - Label':'Work','E-mail 1 - Value':'zoe@example.org','Phone 1 - Label':'Mobile','Phone 1 - Value':'+1 202-555-0147','Address 1 - Postal Code':'00501','Notes':'Alternate email (notes only): zoe.alt@example.org; quoted comma, preserved','Labels':'Wiki5 Fictional Fixture'},
{'First Name':'Márton','Last Name':'Kovács','E-mail 1 - Label':'Home','E-mail 1 - Value':'marton@example.org','Phone 1 - Label':'Mobile','Phone 1 - Value':'+1 202-555-0184','Address 1 - Postal Code':'00042','Notes':'Alternate email (notes only): marton.alt@example.org; literal "quotes"','Labels':'Wiki5 Fictional Fixture'},
{'First Name':'Renée','Last Name':"D'Arcy",'E-mail 1 - Label':'Home','E-mail 1 - Value':'renee@example.org','Phone 1 - Label':'Mobile','Phone 1 - Value':'+1 202-555-0108','Address 1 - Postal Code':'01234','Notes':'Alternate email (notes only): renee.alt@example.org; naïve résumé','Labels':'Wiki5 Fictional Fixture'}]
EXPECTED = [{key:row.get(key,'') for key in HEADER} for row in FIXTURE]
def encode(rows):
stream=io.StringIO(newline='')
writer=csv.DictWriter(stream,fieldnames=HEADER,lineterminator='\r\n')
writer.writeheader(); writer.writerows(rows)
return stream.getvalue().encode('utf-8')
def check(data,expected=None):
if data.startswith(b'\xef\xbb\xbf'): raise ValueError('case requires UTF-8 without BOM')
rows=list(csv.reader(io.StringIO(data.decode('utf-8'),newline=''),strict=True))
if not rows or rows[0]!=HEADER: raise ValueError('header differs from pinned template')
records=[]
for number,row in enumerate(rows[1:],2):
if len(row)!=len(HEADER): raise ValueError('incorrect field count at row '+str(number))
if not any(value.strip() for value in row): raise ValueError('empty row under case contract')
records.append(dict(zip(HEADER,row)))
if not records: raise ValueError('no records')
if expected is not None and records!=expected: raise ValueError('fields differ from expected fixture')
return records
def reproduce():
data=encode(EXPECTED)
tests=[('valid',data,True),('wrong-header',data.replace(b'First Name',b'Given Name',1),False),('duplicate-header',data.replace(b'Last Name',b'First Name',1),False),('extra-column',data.replace(b'Zo\xc3\xab',b'extra,Zo\xc3\xab',1),False),('invalid-utf8',data.replace(b'Zo\xc3\xab',b'Zo\xff',1),False),('unterminated-quote',(','.join(HEADER)+'\r\n"broken').encode(),False),('bom',b'\xef\xbb\xbf'+data,False),('lost-leading-zero',data.replace(b'00501',b'501',1),False)]
result=[]
for name,payload,wanted in tests:
Path(name+'.csv').write_bytes(payload)
try: check(payload,EXPECTED); accepted,reason=True,'all expected fields matched'
except (ValueError,csv.Error) as exc: accepted,reason=False,str(exc)
if accepted!=wanted: raise AssertionError(name+': unexpected outcome')
result.append({'case':name,'expected_accept':wanted,'actual_accept':accepted,'reason':reason,'sha256':hashlib.sha256(payload).hexdigest()})
observed=check(data,EXPECTED)
if encode(observed)!=data: raise AssertionError('round-trip bytes differ')
report={'checked_at_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'python':platform.python_version(),'fixture_sha256':hashlib.sha256(data).hexdigest(),'fixture_bytes':len(data),'expected':EXPECTED,'actual':observed,'round_trip_bytes_equal':True,'tests':result,'product_import':'not run','web_retry_behavior':'unknown'}
Path('test-report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps({k:report[k] for k in ('python','fixture_sha256','fixture_bytes','round_trip_bytes_equal')}))
if __name__=='__main__':
if len(sys.argv)==2: print('Structural preflight only: %s records'%len(check(Path(sys.argv[1]).read_bytes())))
elif len(sys.argv)==1: reproduce()
else: raise SystemExit('Usage: python check_contacts.py [file.csv]')