U.S. Census Data API in Python#

by Avery Fernandez

The U.S. Census Data API provides programmatic access to demographic, economic, and geographic data collected by the U.S. Census Bureau. It enables users to retrieve and analyze a wide variety of data sets, including Census surveys and population statistics.

Please see the following resources for more information on API usage:

NOTE: The U.S. Census Bureau Data API limits requests to a maximum of 500 calls per IP address per day without an API key; however, users can request an API key for increased limits.

These recipe examples were tested on May 7, 2025.

Setup#

Import Libraries#

The following external libraries need to be installed into your environment to run the code examples in this tutorial:

We import the libraries used in this tutorial below:

from time import sleep
import requests
from pprint import pprint
import matplotlib.pyplot as plt
from dotenv import load_dotenv
import os

Import API Key#

An API key is required to access the U.S. Census Data API. You can sign up for one at the Key Signup page.

We keep our API key in a separate file, a .env file, and use the dotenv library to access it. If you use this method, create a file named .env in the same directory as this notebook and add the following line to it:

CENSUS_API_KEY=PUT_YOUR_API_KEY_HERE
load_dotenv()
try:
    API_KEY = os.environ["CENSUS_API_KEY"]
except KeyError:
    print("API key not found. Please set 'CENSUS_API_KEY' in your .env file.")
else:
    print("Environment and API key successfully loaded.")
Environment and API key successfully loaded.

1. Get Population Estimates of Counties by State#

Note: includes Washington, D.C. and Puerto Rico

For obtaining data from the Census API, it is helpful to first obtain a list of state IDs:

# Set the base URL that will be used throughout this tutorial
BASE_URL = "https://api.census.gov/data/"
# The parameters specify what data we want to retrieve
params = {
    "get": "NAME",
    "for": "state:*",   # This will grab the names of all states in the US
    "key": API_KEY
}

# Make the request to the Census API
try:
    response = requests.get(BASE_URL + "2019/pep/population", params=params)
    # Raise an error for bad responses
    response.raise_for_status()  
    # Get the JSON data from the response
    state_ids = response.json()
    print(len(state_ids))
# Handle any errors that occur during the request
except requests.exceptions.RequestException as e:
    print(f"An error occurred: {e}")
    state_ids = None
53
# Show first 10
if state_ids:
    pprint(state_ids[:10])  
[['NAME', 'state'],
 ['Alabama', '01'],
 ['Alaska', '02'],
 ['Arizona', '04'],
 ['Arkansas', '05'],
 ['California', '06'],
 ['Colorado', '08'],
 ['Delaware', '10'],
 ['District of Columbia', '11'],
 ['Connecticut', '09']]
# Remove heading from data
if state_ids:
    state_ids = state_ids[1:]
    pprint(state_ids[0:10])
[['Alabama', '01'],
 ['Alaska', '02'],
 ['Arizona', '04'],
 ['Arkansas', '05'],
 ['California', '06'],
 ['Colorado', '08'],
 ['Delaware', '10'],
 ['District of Columbia', '11'],
 ['Connecticut', '09'],
 ['Florida', '12']]

Now we can loop through each state and pull their individual population data:

# Loop through the states and get the population for each county in each state
state_population = {}

# Check to make sure state_ids is not None before proceeding
if state_ids:
    # Loop through each state
    for states in state_ids:
        state_name = states[0]

        params = {
            "get": "NAME,POP",
            "for": "county:*",
            "in": f"state:{states[1]}",
            "key": API_KEY
        }
        try:
            response = requests.get(BASE_URL + "2019/pep/population", params=params)
            sleep(1)  
            # Raise an error for bad responses
            response.raise_for_status()  
            data = response.json()[1:]
        except requests.exceptions.RequestException as e:
            print(f"An error occurred: {e}")
            data = None

        # Create a dictionary to hold the population of counties in the state
        counties_population = {}
        if data:
            for county in data:
                # Extract county name by removing state name and the comma
                county_name = county[0][:-len(state_name)-2]
                # Extract county population, converting to int or NaN if not available
                county_population = int(county[1] or 'nan')
                # Add the county name and population to the dictionary
                counties_population[county_name] = county_population
            # Add the counties population to the state population dictionary
            state_population[state_name] = counties_population
# Show data for Alabama
state_population["Alabama"]
{'St. Clair County': 89512,
 'Cullman County': 83768,
 'Houston County': 105882,
 'Tuscaloosa County': 209355,
 'Coffee County': 52342,
 'Chilton County': 44428,
 'Coosa County': 10663,
 'Etowah County': 102268,
 'Lamar County': 13805,
 'Butler County': 19448,
 'Walker County': 63521,
 'Greene County': 8111,
 'Bullock County': 10101,
 'Chambers County': 33254,
 'Monroe County': 20733,
 'Lawrence County': 32924,
 'Lee County': 164542,
 'Marion County': 29709,
 'Pickens County': 19930,
 'Sumter County': 12427,
 'Jefferson County': 658573,
 'Choctaw County': 12589,
 'Franklin County': 31362,
 'Marengo County': 18863,
 'Russell County': 57961,
 'Cherokee County': 26196,
 'Covington County': 37049,
 'Crenshaw County': 13772,
 'Dallas County': 37196,
 'Lauderdale County': 92729,
 'Lowndes County': 9726,
 'Macon County': 18068,
 'Limestone County': 98915,
 'Shelby County': 217702,
 'Winston County': 23629,
 'Baldwin County': 223234,
 'Elmore County': 81209,
 'Jackson County': 51626,
 'Talladega County': 79978,
 'Washington County': 16326,
 'Clay County': 13235,
 'Morgan County': 119679,
 'Pike County': 33114,
 'Colbert County': 55241,
 'Dale County': 49172,
 'Hale County': 14651,
 'DeKalb County': 71513,
 'Escambia County': 36633,
 'Randolph County': 22722,
 'Mobile County': 413210,
 'Perry County': 8923,
 'Cleburne County': 14910,
 'Conecuh County': 12067,
 'Barbour County': 24686,
 'Bibb County': 22394,
 'Calhoun County': 113605,
 'Clarke County': 23622,
 'Fayette County': 16302,
 'Montgomery County': 226486,
 'Marshall County': 96774,
 'Blount County': 57826,
 'Henry County': 17205,
 'Madison County': 372909,
 'Autauga County': 55869,
 'Tallapoosa County': 40367,
 'Geneva County': 26271,
 'Wilcox County': 10373}

2. Get Population Estimates Over a Range of Years#

We can use similar code as before, but now loop through different population estimate datasets by year. Here are the specific APIs used:

Vintage 2015 Population Estimates

Vintage 2016 Population Estimates

Vintage 2017 Population Estimates

state_population = {}

if state_ids:
    # Goes through datasets for 2015, 2016, 2017
    for states in state_ids:

        annual_populations = {}
        for year in range(2015, 2018):
            stateName = states[0]
            params = {
                "get": "GEONAME,POP",
                "for": "county:*",
                "in": f"state:{states[1]}",
                "key": API_KEY
            }

            try:
                response = requests.get(
                    BASE_URL + str(year) + "/pep/population", 
                    params=params
                )
                sleep(1)  
                # Raise an error for bad responses
                response.raise_for_status()  
                data = response.json()[1:]
            except requests.exceptions.RequestException as e:
                print(f"An error occurred: {e}")
                data = None

            counties_population = {}
            if data:
                for county in data:
                    county_name = county[0][:-len(stateName)-2]
                    county_population = int(county[1] or 'nan')
                    counties_population[county_name] = county_population

            annual_populations[year] = counties_population
        state_population[stateName] = annual_populations
# Show Alabama data
pprint(state_population["Alabama"][2015])
{'Autauga County, Alabama, East South Central, South, Unit': 55347,
 'Baldwin County, Alabama, East South Central, South, Unit': 203709,
 'Barbour County, Alabama, East South Central, South, Unit': 26489,
 'Bibb County, Alabama, East South Central, South, Unit': 22583,
 'Blount County, Alabama, East South Central, South, Unit': 57673,
 'Bullock County, Alabama, East South Central, South, Unit': 10696,
 'Butler County, Alabama, East South Central, South, Unit': 20154,
 'Calhoun County, Alabama, East South Central, South, Unit': 115620,
 'Chambers County, Alabama, East South Central, South, Unit': 34123,
 'Cherokee County, Alabama, East South Central, South, Unit': 25859,
 'Chilton County, Alabama, East South Central, South, Unit': 43943,
 'Choctaw County, Alabama, East South Central, South, Unit': 13170,
 'Clarke County, Alabama, East South Central, South, Unit': 24675,
 'Clay County, Alabama, East South Central, South, Unit': 13555,
 'Cleburne County, Alabama, East South Central, South, Unit': 15018,
 'Coffee County, Alabama, East South Central, South, Unit': 51211,
 'Colbert County, Alabama, East South Central, South, Unit': 54354,
 'Conecuh County, Alabama, East South Central, South, Unit': 12672,
 'Coosa County, Alabama, East South Central, South, Unit': 10724,
 'Covington County, Alabama, East South Central, South, Unit': 37835,
 'Crenshaw County, Alabama, East South Central, South, Unit': 13963,
 'Cullman County, Alabama, East South Central, South, Unit': 82005,
 'Dale County, Alabama, East South Central, South, Unit': 49565,
 'Dallas County, Alabama, East South Central, South, Unit': 41131,
 'DeKalb County, Alabama, East South Central, South, Unit': 71130,
 'Elmore County, Alabama, East South Central, South, Unit': 81468,
 'Escambia County, Alabama, East South Central, South, Unit': 37789,
 'Etowah County, Alabama, East South Central, South, Unit': 103057,
 'Fayette County, Alabama, East South Central, South, Unit': 16759,
 'Franklin County, Alabama, East South Central, South, Unit': 31696,
 'Geneva County, Alabama, East South Central, South, Unit': 26777,
 'Greene County, Alabama, East South Central, South, Unit': 8479,
 'Hale County, Alabama, East South Central, South, Unit': 15068,
 'Henry County, Alabama, East South Central, South, Unit': 17221,
 'Houston County, Alabama, East South Central, South, Unit': 104173,
 'Jackson County, Alabama, East South Central, South, Unit': 52419,
 'Jefferson County, Alabama, East South Central, South, Unit': 660367,
 'Lamar County, Alabama, East South Central, South, Unit': 13886,
 'Lauderdale County, Alabama, East South Central, South, Unit': 92596,
 'Lawrence County, Alabama, East South Central, South, Unit': 33115,
 'Lee County, Alabama, East South Central, South, Unit': 156993,
 'Limestone County, Alabama, East South Central, South, Unit': 91663,
 'Lowndes County, Alabama, East South Central, South, Unit': 10458,
 'Macon County, Alabama, East South Central, South, Unit': 19105,
 'Madison County, Alabama, East South Central, South, Unit': 353089,
 'Marengo County, Alabama, East South Central, South, Unit': 20028,
 'Marion County, Alabama, East South Central, South, Unit': 30168,
 'Marshall County, Alabama, East South Central, South, Unit': 94725,
 'Mobile County, Alabama, East South Central, South, Unit': 415395,
 'Monroe County, Alabama, East South Central, South, Unit': 21673,
 'Montgomery County, Alabama, East South Central, South, Unit': 226519,
 'Morgan County, Alabama, East South Central, South, Unit': 119565,
 'Perry County, Alabama, East South Central, South, Unit': 9652,
 'Pickens County, Alabama, East South Central, South, Unit': 20864,
 'Pike County, Alabama, East South Central, South, Unit': 33046,
 'Randolph County, Alabama, East South Central, South, Unit': 22696,
 'Russell County, Alabama, East South Central, South, Unit': 59660,
 'Shelby County, Alabama, East South Central, South, Unit': 208713,
 'St. Clair County, Alabama, East South Central, South, Unit': 87074,
 'Sumter County, Alabama, East South Central, South, Unit': 13103,
 'Talladega County, Alabama, East South Central, South, Unit': 80862,
 'Tallapoosa County, Alabama, East South Central, South, Unit': 40844,
 'Tuscaloosa County, Alabama, East South Central, South, Unit': 203976,
 'Walker County, Alabama, East South Central, South, Unit': 65294,
 'Washington County, Alabama, East South Central, South, Unit': 16804,
 'Wilcox County, Alabama, East South Central, South, Unit': 11059,
 'Winston County, Alabama, East South Central, South, Unit': 23877}

3. Plot Population Change#

This data is based off the 2021 Population Estimates dataset.

The percentage change in population is from July 1, 2020 to July 1, 2021 for states (includes Washington, D.C. and Puerto Rico)

params = {
    "get": "NAME,POP_2021,PPOPCHG_2021",
    "for": "state:*",
    "key": API_KEY
}

try:
    response = requests.get(BASE_URL + "2021/pep/population", params=params)
    # Raise an error for bad responses
    response.raise_for_status()  
    data = response.json()[1:]
    # Sort by the state name
    data.sort()
    print(len(data))
except requests.exceptions.RequestException as e:
    print(f"An error occurred: {e}")
    data = None
52
if data:
    pprint(data[0:10])
[['Alabama', '5039877', '0.2999918604', '01'],
 ['Alaska', '732673', '0.0316749062', '02'],
 ['Arizona', '7276316', '1.3698828613', '04'],
 ['Arkansas', '3025891', '0.4534511286', '05'],
 ['California', '39237836', '-0.6630474360', '06'],
 ['Colorado', '5812069', '0.4799364073', '08'],
 ['Connecticut', '3605597', '0.1482392938', '09'],
 ['Delaware', '1003384', '1.1592057958', '10'],
 ['District of Columbia', '670050', '-2.9043911470', '11'],
 ['Florida', '21781128', '0.9791222337', '12']]
# Prepare data for plotting
stateName = []
population = []
populationChange = []
for states in data:
    stateName.append(states[0])
    population.append(int(states[1]) or 'nan')
    populationChange.append(float(states[2] or 'nan'))
# Create the figure and axis objects
fig, ax = plt.subplots(figsize=(10, 15))

# Create the scatter plot with enhanced marker style
ax.scatter(
    populationChange, 
    stateName, 
    color="#1f77b4",
    alpha=0.7,
    s=50,
    edgecolor='white',
    linewidth=0.8
)

# Add a vertical line at x=0 for reference
ax.axvline(0, color='gray', linestyle='--', linewidth=1)

# Set titles and labels with larger fonts and bold title for emphasis
ax.set_title("Population Change from 2020 to 2021", fontsize=18, fontweight='bold', color='k')
ax.set_xlabel("% Population Change", fontsize=14, color='k')
ax.set_ylabel("States (including Washington DC and Puerto Rico)", fontsize=14, color='k')

# Remove top and right spines for a cleaner appearance
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# Increase tick label sizes
ax.tick_params(axis='both', which='major', labelsize=12)

# Improve spacing for a tight layout
plt.tight_layout()

# Display the plot
plt.show()
../../_images/f54cec30d81df591b3f05cabced2b3dc3d46b79ec50955c273a969462508fe90.png