Split a full-name CSV into CRM columns with Python

Turn a full-name column into suggested first, middle, and last names. Keep the originals.

PythonLast tested Get the complete example ↓
On this page

Run the example

Download the complete source below. In a macOS or Linux terminal, install the SDK, set your secret API key, then run the file.

Install
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install parseapi
API key
export PARSEAPI_KEY='YOUR_SECRET_API_KEY'
Run
python3 full-name-csv.python.py

Split the full-name column

A CSV has full_name; your CRM needs separate name fields. This script appends suggested parts and keeps the original names beside them.

Use Python 3.9 or later. Install the SDK and set your secret PARSEAPI_KEY with the setup above.

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

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.

Complete example

The complete Python source. The script reads your API key from PARSEAPI_KEY.

Download Python
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? Create a secret API key. For every field and parameter, see Name API reference.

Build something else

All tutorials →