Split full names into CRM columns

Split full names into suggested first, middle, and last name columns with Python for a reviewed CRM import.

PythonLast tested Download and run ↓
On this page

Run the example

Download Python

full-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.

Terminal
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.py

The 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.

Need a key? Set up this example

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:

csv
id,full_name
001,GRACE HOPPER
002,"Smith, John"
003,OSCAR DE LA HOYA
004,GRACE HOPPER
005,
006,test test

Run the downloaded script:

Terminal
python3 full-name-csv.python.py contacts.csv --output contacts-parsed.csv

The 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

Python
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:

csv
id,parse_first,parse_last,parse_status
001,Grace,Hopper,parsed
005,,,missing_name
Python
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})

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
full-name-csv.python.py
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.

Build something else

All tutorials →