Skip to main content

Enrich a CSV of addresses with Census demographics (Python)

Turn a plain list of US addresses into an enriched table with rooftop coordinates, the 15-digit Census block, and official Census demographics — in about 40 lines of Python, using the AddressCanvas API.

This is the exact workflow behind demographic enrichment, HMDA/fair-lending prep, and market analysis: geocode each address, get its Census geography, then pull the numbers for that geography.

What you'll need

  • A free account at addresscanvas.com and an API key (create one on your dashboard). New accounts get 5,000 geocodes/month plus 150 free credits on first sign-in.

  • Python 3.9+ and requests (pip install requests).

  • A CSV with an address column, e.g.:

    address
    "1600 Pennsylvania Ave NW, Washington, DC 20500"
    "782 N Lakewood Ave, Ocoee, FL 34761"
    

The two calls

Both endpoints are POST /api/v1/..., take {"address": "..."}, and authenticate with an X-API-Key: gck_... header.

1. Geocode — resolve the address to a point and its Census block:

curl -X POST "https://addresscanvas.com/api/v1/geocode" \
  -H "X-API-Key: gck_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"address": "1600 Pennsylvania Ave NW, Washington, DC 20500"}'
{
  "matchCode": "BUILDING_PARCEL",
  "locationCode": "ROOFTOP",
  "lat": 38.89594,
  "lon": -77.037419,
  "fips": "11001",
  "censusBlock": "110019800001034"
}

That censusBlock is a 2020 GEOID: state (2) + county (3) + tract (6) + block (4). Slice it for tract or block group.

2. Demographics — pull Census/ACS data for that address (20 credits per successful report; nothing is charged when data isn't available):

curl -X POST "https://addresscanvas.com/api/v1/demographics" \
  -H "X-API-Key: gck_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"address": "1600 Pennsylvania Ave NW, Washington, DC 20500"}'

The script

Save this as enrich.py:

import csv
import os
import sys

import requests

BASE = "https://addresscanvas.com"
HEADERS = {"X-API-Key": os.environ["ADDRESSCANVAS_API_KEY"],
           "Content-Type": "application/json"}

def post(path, address):
    r = requests.post(f"{BASE}{path}", headers=HEADERS,
                      json={"address": address}, timeout=30)
    r.raise_for_status()
    return r.json()

def enrich(address):
    geo = post("/api/v1/geocode", address)
    row = {"lat": geo.get("lat"), "lon": geo.get("lon"),
           "census_block": geo.get("censusBlock"), "fips": geo.get("fips")}
    # Only spend credits on demographics if the address actually matched.
    if geo.get("matchCode") not in (None, "NO_MATCH"):
        demo = post("/api/v1/demographics", address)
        if demo.get("available"):
            row["population"] = demo["population"]["total"]
            row["median_household_income"] = demo["income"]["medianHouseholdIncome"]
    return row

def main(in_path, out_path):
    with open(in_path, newline="", encoding="utf-8") as src:
        rows = list(csv.DictReader(src))
    extra = ["lat", "lon", "census_block", "fips",
             "population", "median_household_income"]
    with open(out_path, "w", newline="", encoding="utf-8") as dst:
        writer = csv.DictWriter(dst, fieldnames=list(rows[0]) + extra)
        writer.writeheader()
        for row in rows:
            row.update(enrich(row["address"]))
            writer.writerow(row)

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

The demographics response nests its figures by topic: the population is population.total and the income is income.medianHouseholdIncome. The same response carries education, housing, raceEthnicity, and a block group / tract / county comparison.

Run it over a file:

export ADDRESSCANVAS_API_KEY=gck_your_api_key
python enrich.py addresses.csv enriched.csv

Notes & good habits

  • A no-match is an HTTP 200, not an error — check matchCode == "NO_MATCH" (with a noMatchReason) rather than relying on the status code.
  • Guard your credits. The script only calls /demographics when the geocode matched, so you never spend credits on an address that didn't resolve.
  • You own the output. Unlike Google Maps, there's no restriction on storing the coordinates or GEOIDs you get back — write them to your warehouse and keep them.
  • Larger files. For thousands of rows, geocode 100 addresses per call with POST /api/v1/geocode/batch; Geocode a large address list shows the pattern. Reports are still one request per address.

Where to go next

  • Swap /demographics for /demographic-trend, /schools, /hazards, /crime, or /property-intel to enrich with other datasets on the same address.
  • See per-state rooftop coverage at /coverage/states.

Run this on your own addresses

Start free with 5,000 geocodes a month, Census block included.

Get your API key — 150 free credits