Split full names into CRM columns
Split full names into suggested first, middle, and last name columns with Python for a reviewed CRM import.
On this page
Run the example
Download Pythonfull-name-csv.python.py
Download the file, then run these commands in its folder using a macOS or Linux terminal. You need Python 3.9 or later and a secret API key.
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install parseapi
export PARSEAPI_KEY='YOUR_SECRET_API_KEY'
python3 full-name-csv.python.pyThe first run uses the included sample. The CSV instructions below show how to save your own results. Review rows can produce exit code 2 even when the output was written successfully.
Split the full-name column
Your contact CSV has a full_name column, but the CRM import expects first and last names. Use this Python script to split full names into suggested parts while keeping the original spelling beside them.
1. Run your file
Save this as contacts.csv:
id,full_name
001,GRACE HOPPER
002,"Smith, John"
003,OSCAR DE LA HOYA
004,GRACE HOPPER
005,
006,test testRun the downloaded script:
python3 full-name-csv.python.py contacts.csv --output contacts-parsed.csvThe output filename must be new. Omit the input filename to use the included sample. Without --output, CSV goes to standard output; never redirect onto the input file.
2. Parse each name once
result = parsed(parse.name(value))The Name API handles the parsing. "Smith, John" produces first John, last Smith. OSCAR DE LA HOYA produces Oscar and de la Hoya.
Repeated names reuse a lookup while keeping separate rows. Only names go to ParseAPI; IDs and other columns stay in the script.
3. Review the suggested parts
The output keeps full_name and appends suggested parts. Here are selected columns:
id,parse_first,parse_last,parse_status
001,Grace,Hopper,parsed
005,,,missing_namestatus = "parsed" if data["first"] and data["last"] else "review_name_parts"
return outcome(status, http_status=200,
**{"parse_" + key: data[key] or "" for key in PARTS})New columns are parse_name, parse_prefix, parse_first, parse_middle, parse_last, parse_suffix, and three status/error columns. Missing parts stay empty.
parsed means first and last parts were returned. review_name_parts retains suggestions with an incomplete split; a person can legitimately have one name. Blank, oversized, and invalid names receive their own review status. A failed request stays error, never an invalid-name verdict.
Preserve full_name and let the person correct the parts. Parsing cannot establish identity or preferred spelling. See the W3C guidance on personal names.
The complete script preserves original cells, row order, quoted commas, and UTF-8 text. Use a full_name header, distinct nonempty columns, and 1-200 rows within 64 KiB. Malformed rows and existing output columns are rejected before requests. Calls use a ten-second timeout without retries; account/rate errors stop new lookups.
Exit 0 means every row parsed, 2 means the written file includes review rows, and 1 means a startup or file failure. Review the appended fields before importing them into your CRM. Use Bulk Name for a larger file.
Complete example
The complete Python source. The script reads your API key from PARSEAPI_KEY.
View and copy the complete source
import argparse
import csv
import io
import sys
from pathlib import Path
from parseapi import ParseAPI, ParseAPIError
DESCRIPTION = "Split a full-name CSV into suggested CRM fields."
REQUIRED = ["full_name"]
PARTS = ["name", "prefix", "first", "middle", "last", "suffix"]
COLUMNS = ["parse_" + name for name in PARTS] + ["parse_status", "parse_error", "parse_http_status"]
SAMPLE = """id,full_name,note
001,GRACE HOPPER,Original spelling retained
002,"Smith, John",Comma order
003,OSCAR DE LA HOYA,Family-name particle
004,GRACE HOPPER,Repeated name
005,,Missing name
006,test test,Review this row
"""
# example:read
def read_rows(stream):
reader = csv.reader(stream, strict=True)
fields = next(reader, None)
if (not fields or not set(REQUIRED).issubset(fields)
or any(not field.strip() for field in fields)
or len(fields) != len(set(fields))):
raise ValueError("Use distinct, nonempty headers including " + ", ".join(REQUIRED) + ".")
if set(fields) & set(COLUMNS):
raise ValueError("The input already contains output columns. Use the original CSV.")
rows = []
for cells in reader:
if len(cells) != len(fields):
raise ValueError("Every row must have the same number of cells as the header.")
rows.append(dict(zip(fields, cells)))
if len(rows) > 200:
raise ValueError("Use 1 to 200 data rows for this example.")
if not rows:
raise ValueError("The CSV needs at least one data row.")
return fields, rows
# /example:read
def outcome(status, error="", http_status="", **values):
return {**dict.fromkeys(COLUMNS, ""), "parse_status": status,
"parse_error": error, "parse_http_status": http_status, **values}
def write_rows(stream, fields, rows):
writer = csv.DictWriter(stream, fieldnames=fields + COLUMNS)
writer.writeheader()
writer.writerows(rows)
def parsed(data):
if not isinstance(data, dict) or type(data.get("valid")) is not bool:
return outcome("error", "unexpected_response")
if any(key not in data or (data[key] is not None and
(not isinstance(data[key], str) or not data[key].strip())) for key in PARTS):
return outcome("error", "unexpected_response")
if not data["valid"]:
return outcome("invalid_name", http_status=200)
if not data["name"]:
return outcome("error", "unexpected_response")
# example:result
status = "parsed" if data["first"] and data["last"] else "review_name_parts"
return outcome(status, http_status=200,
**{"parse_" + key: data[key] or "" for key in PARTS})
# /example:result
def enrich(rows, parse):
results, cache, stopped = [], {}, False
for row in rows:
value = row["full_name"].strip()
if not value:
result = outcome("missing_name")
elif len(value.encode("utf-16-le")) // 2 > 120:
result = outcome("review_name_length")
elif value in cache:
result = cache[value]
elif stopped:
result = outcome("not_attempted")
else:
try:
# example:request
result = parsed(parse.name(value))
# /example:request
except ParseAPIError as error:
result = outcome("error", error.code, error.status)
stopped = error.status in (401, 403, 429)
except Exception:
result = outcome("error", "request_failed")
cache[value] = result
results.append({**row, **result})
return results
def main():
parser = argparse.ArgumentParser(description=DESCRIPTION)
parser.add_argument("input", nargs="?", help="UTF-8 CSV filename, or omit to run the sample")
parser.add_argument("--output", help="New CSV filename, or omit for standard output")
args = parser.parse_args()
try:
if args.output and (Path(args.output).exists() or Path(args.output).is_symlink()):
raise ValueError("The output path already exists. Choose a new filename.")
if args.input:
path = Path(args.input)
if path.stat().st_size > 65536:
raise ValueError("Use a CSV no larger than 64 KiB.")
with path.open(encoding="utf-8-sig", newline="") as stream:
fields, rows = read_rows(stream)
else:
fields, rows = read_rows(io.StringIO(SAMPLE))
# Reads PARSEAPI_KEY. Reuse one client without automatic retries.
with ParseAPI(timeout=10.0, retries=0) as parse:
results = enrich(rows, parse)
if args.output:
# Exclusive creation protects against a file appearing during the lookups.
with Path(args.output).open("x", encoding="utf-8", newline="") as stream:
write_rows(stream, fields, results)
else:
write_rows(sys.stdout, fields, results)
except (ValueError, OSError, csv.Error) as error:
print(f"Could not complete: {error}", file=sys.stderr)
return 1
review = sum(row["parse_status"] != "parsed" for row in results)
print(f"{len(results)} rows written, {review} need review.", file=sys.stderr)
return 2 if review else 0
if __name__ == "__main__":
sys.exit(main())
Need a key? Set up this server example. For every field and parameter, see Name API reference.