Census API Python Tutorial: Regional Analysis Guide
Learn the Census API with Python. Download ACS data, compare U.S. regions, states and counties, handle FIPS codes, build maps, and create a regional notebook.
Census API with Python: Download ACS Data into pandas
The U.S. Census Bureau provides an API that lets you download official population, income, poverty, education, housing, and other statistics directly into Python.
In this tutorial, you will use the Census Data API with Python and pandas. You will learn how to request American Community Survey data, find the right variables, download state and county data, preserve FIPS codes, work with margins of error, and build a reusable API function.
The examples use the 2024 American Community Survey 5-year Data Profiles.
By the end, you will be able to turn a Census API request into a clean pandas DataFrame that is ready for analysis.
Table of contents
- Quick Census API example
- What is the Census Data API?
- Why use the ACS?
- What 2024 ACS 5-year means
- Get a Census API key
- Install Python packages
- Understand a Census API request
- Find Census variables
- Margins of error
- Make your first state request
- Convert Census JSON to pandas
- Keep FIPS codes as strings
- Build a reusable Census function
- Understand Census geography
- Download county data
- Compare ACS data over time
- Validate Census data
- Common errors
- Frequently asked questions
Quick example: download Census data with Python
If you already have a Census API key, this example downloads population and median household income for every state.
import os
import requests
import pandas as pd
url = (
"https://api.census.gov/data/"
"2024/acs/acs5/profile"
)
params = {
"get": "NAME,DP05_0001E,DP03_0062E",
"for": "state:*",
"key": os.getenv("CENSUS_API_KEY"),
}
response = requests.get(
url,
params=params,
timeout=30,
)
response.raise_for_status()
rows = response.json()
df = pd.DataFrame(
rows[1:],
columns=rows[0],
)
df["DP05_0001E"] = pd.to_numeric(
df["DP05_0001E"],
errors="coerce",
)
df["DP03_0062E"] = pd.to_numeric(
df["DP03_0062E"],
errors="coerce",
)
print(df.head())
The Census API returns JSON as a two-dimensional array. The first row contains column names and the remaining rows contain observations.
In this request:
DP05_0001Eis total population.DP03_0062Eis median household income in 2024 inflation-adjusted dollars.state:*means all supported state-level areas.
What is the Census Data API?
The Census Data API gives developers and researchers programmatic access to statistical data published by the U.S. Census Bureau.
Instead of downloading a large spreadsheet and manually finding the rows you need, you can request specific variables for specific geographic areas.
A Census API request usually contains four main ideas:
- Vintage: the release year you want.
- Dataset: the Census product you want.
- Variables: the statistics you want returned.
- Geography: the places you want returned.
For example:
https://api.census.gov/data/2024/acs/acs5/profile
Here, 2024 is the vintage and acs/acs5/profile is the ACS 5-year Data Profiles dataset.
The Census Bureau also provides TIGERweb services for geographic boundaries and a Geocoder API for translating addresses into geographic coordinates. These are separate from the statistical Data API used in this tutorial.
Why use the American Community Survey?
The American Community Survey, usually called the ACS, provides detailed information about people, households, housing, income, education, employment, poverty, and many other subjects.
Two important ACS products are the ACS 1-year and ACS 5-year estimates.
| Product | Data period | Geography coverage | Main use |
|---|---|---|---|
| ACS 1-year | 12 months | Larger geographic areas | More current estimates |
| ACS 5-year | 60 months | All geographic areas | Smaller areas and broader coverage |
This tutorial uses the 2024 ACS 5-year product because it works well for learning state and county geography with one consistent dataset.
Understand what "2024 ACS 5-year" means
The year in the dataset name can be confusing.
The 2024 ACS 5-year estimates do not describe only the calendar year 2024.
They are based on data collected from January 1, 2020 through December 31, 2024.
So if the dataset reports a county poverty rate of 14%, the correct interpretation is that it is an estimate for the 2020 to 2024 ACS period.
This distinction matters when you compare ACS data with annual economic statistics from other sources.
Get a Census API key
Current Census developer documentation requires an API key for data queries.
Request a key from the Census Bureau and store it outside your Python code.
Do not publish your real API key in GitHub, a notebook, a screenshot, or an article.
macOS or Linux
export CENSUS_API_KEY="your_api_key"
Windows PowerShell
$env:CENSUS_API_KEY="your_api_key"
Read it in Python:
import os
api_key = os.getenv("CENSUS_API_KEY")
if not api_key:
raise RuntimeError(
"CENSUS_API_KEY is not set."
)
Using an environment variable keeps the credential separate from your source code.
Install the Python packages
python -m pip install requests pandas matplotlib
| Package | Purpose |
|---|---|
requests |
Sends API requests |
pandas |
Cleans and analyzes Census data |
matplotlib |
Creates charts |
You do not need a special Census Python package to call the API.
Understand a Census API request
Consider this request:
https://api.census.gov/data/2024/acs/acs5/profile
?get=NAME,DP05_0001E,DP03_0062E
&for=state:*
&key=YOUR_KEY
| Part | Meaning |
|---|---|
2024 |
Data vintage |
acs/acs5/profile |
ACS 5-year Data Profiles |
get |
Variables to return |
for |
Main geography |
in |
Parent geography when needed |
key |
Census API key |
A standard Census API call can include up to 50 variables.
Requesting only the variables you need keeps the response easier to understand.
How to find Census variables
Census variable names can look difficult at first.
For example:
DP03_0062E
The safest approach is to inspect the official variable metadata for the exact vintage and dataset you are using.
For the 2024 ACS 5-year Data Profiles, some useful variables are:
| Variable | Meaning |
|---|---|
DP05_0001E |
Total population |
DP05_0001M |
Total population margin of error |
DP03_0062E |
Median household income |
DP03_0062M |
Median household income margin of error |
DP03_0128PE |
Percent of people below poverty level |
DP03_0128PM |
Poverty percentage margin of error |
DP02_0068PE |
Percent age 25+ with bachelor's degree or higher |
DP02_0068PM |
Education percentage margin of error |
What E, M, PE, and PM mean
| Ending | Meaning |
|---|---|
E |
Estimate |
M |
Margin of error |
PE |
Percent estimate |
PM |
Percent margin of error |
Do not treat the estimate and margin of error as the same thing.
Why you should download margins of error
ACS data come from a sample. That means an ACS estimate is not perfectly precise.
Suppose two states have poverty estimates of:
State A: 12.1%
State B: 12.4%
It is tempting to say State B definitely has a higher poverty rate.
But if the margins of error overlap substantially, that conclusion may be too strong.
For research work, download the matching margin of error whenever it is available.
Instead of requesting only:
DP03_0128PE
request:
DP03_0128PE,DP03_0128PM
The same idea applies to population, income, education, and many other ACS estimates.
Make your first state request
import os
import requests
import pandas as pd
BASE_URL = (
"https://api.census.gov/data/"
"2024/acs/acs5/profile"
)
variables = [
"NAME",
"DP05_0001E",
"DP05_0001M",
"DP03_0062E",
"DP03_0062M",
"DP03_0128PE",
"DP03_0128PM",
"DP02_0068PE",
"DP02_0068PM",
]
params = {
"get": ",".join(variables),
"for": "state:*",
"key": os.getenv("CENSUS_API_KEY"),
}
response = requests.get(
BASE_URL,
params=params,
timeout=30,
)
response.raise_for_status()
rows = response.json()
The asterisk in state:* means return all available state-level areas.
Convert Census JSON to pandas
df = pd.DataFrame(
rows[1:],
columns=rows[0],
)
print(df.head())
At this point, most measurement columns are still strings. That is normal.
Rename the Census variables
VARIABLES = {
"DP05_0001E": "population",
"DP05_0001M": "population_moe",
"DP03_0062E": "median_household_income",
"DP03_0062M": "median_household_income_moe",
"DP03_0128PE": "poverty_rate",
"DP03_0128PM": "poverty_rate_moe",
"DP02_0068PE": "bachelors_or_higher",
"DP02_0068PM": "bachelors_or_higher_moe",
}
df = df.rename(
columns=VARIABLES
)
Convert measurement columns to numbers
numeric_columns = [
"population",
"population_moe",
"median_household_income",
"median_household_income_moe",
"poverty_rate",
"poverty_rate_moe",
"bachelors_or_higher",
"bachelors_or_higher_moe",
]
for column in numeric_columns:
df[column] = pd.to_numeric(
df[column],
errors="coerce",
)
Do not automatically replace missing Census estimates with zero. Missing data and a true estimate of zero mean different things.
Watch for Census annotation values
Some Census products use special values or annotations when an estimate is unavailable, not applicable, suppressed, or needs explanation.
Inspect unexpected values before treating them as real measurements.
Keep FIPS codes as strings
This is one of the most important Census data rules.
A state FIPS code can look like:
06
If Python converts that value to an integer, it becomes 6. The leading zero disappears.
Keep geography codes as strings:
df["state"] = (
df["state"]
.astype("string")
.str.zfill(2)
)
For county data, keep both the two-digit state code and three-digit county code as strings.
Build a reusable Census API function
import os
from typing import Iterable
import pandas as pd
import requests
def census_get(
year: int,
dataset: str,
variables: Iterable[str],
for_clause: str,
in_clause: str | None = None,
) -> pd.DataFrame:
api_key = os.getenv(
"CENSUS_API_KEY"
)
if not api_key:
raise RuntimeError(
"CENSUS_API_KEY is not set."
)
variables = list(variables)
if len(variables) > 50:
raise ValueError(
"A standard Census API query "
"can include up to 50 variables."
)
url = (
f"https://api.census.gov/data/"
f"{year}/{dataset}"
)
params = {
"get": ",".join(variables),
"for": for_clause,
"key": api_key,
}
if in_clause:
params["in"] = in_clause
response = requests.get(
url,
params=params,
timeout=30,
)
response.raise_for_status()
rows = response.json()
if not rows or len(rows) < 2:
return pd.DataFrame()
return pd.DataFrame(
rows[1:],
columns=rows[0],
)
Now the state request becomes:
variables = [
"NAME",
"DP05_0001E",
"DP05_0001M",
"DP03_0062E",
"DP03_0062M",
"DP03_0128PE",
"DP03_0128PM",
]
states = census_get(
year=2024,
dataset="acs/acs5/profile",
variables=variables,
for_clause="state:*",
)
Census geography in plain English
Geography is often the hardest part of the Census API.
Useful examples include:
for=us:1
United States total.
for=region:*
All Census regions.
for=state:*
All states.
for=county:*&in=state:06
All counties in California.
for=tract:*&in=state:06 county:037
All Census tracts in Los Angeles County, California.
The newer ucgid geography option
The current Census API User Guide also documents the ucgid predicate.
For beginners, the normal for and in pattern is usually easier to read. Use ucgid when you need more complex geography combinations.
Download county data for California
California's state FIPS code is:
06
Use it as the parent geography:
counties_ca = census_get(
year=2024,
dataset="acs/acs5/profile",
variables=[
"NAME",
"DP05_0001E",
"DP03_0062E",
"DP03_0128PE",
],
for_clause="county:*",
in_clause="state:06",
)
print(counties_ca.head())
The response includes state and county. Keep both as strings.
counties_ca["state"] = (
counties_ca["state"]
.astype("string")
.str.zfill(2)
)
counties_ca["county"] = (
counties_ca["county"]
.astype("string")
.str.zfill(3)
)
counties_ca["geoid"] = (
counties_ca["state"]
+ counties_ca["county"]
)
A full county GEOID has five digits.
Rank states carefully
Once your columns are numeric, you can sort them. But rankings should be interpreted with care because ACS estimates have sampling uncertainty.
states = states.rename(
columns=VARIABLES
)
states[
"median_household_income"
] = pd.to_numeric(
states[
"median_household_income"
],
errors="coerce",
)
top_income = (
states
.sort_values(
"median_household_income",
ascending=False,
)
[
[
"NAME",
"median_household_income",
]
]
.head(10)
)
print(top_income)
Compare income and poverty
import matplotlib.pyplot as plt
plot_data = states.dropna(
subset=[
"median_household_income",
"poverty_rate",
]
)
fig, ax = plt.subplots(
figsize=(9, 6)
)
ax.scatter(
plot_data[
"median_household_income"
],
plot_data["poverty_rate"],
)
ax.set_xlabel(
"Median household income"
)
ax.set_ylabel(
"People below poverty level, percent"
)
ax.set_title(
"State Household Income and Poverty"
)
ax.grid(alpha=0.25)
fig.tight_layout()
plt.show()
This chart shows an association in the ACS data. It does not show that household income causes a particular poverty rate or that poverty causes income differences.
Find variables before writing the final code
A common mistake is finding a Census variable code in a blog post and copying it without checking the exact vintage.
A good workflow is:
- Choose the Census vintage.
- Choose the dataset.
- Open its variable metadata.
- Search for the measure you need.
- Read the full label.
- Check whether you need an estimate, percentage, or margin of error.
- Save the variable mapping in your project.
How to compare ACS 5-year estimates over time
The 2024 ACS 5-year estimate covers 2020 to 2024.
If you compare it directly with 2019 to 2023, the two periods share four years of data.
For longer-term comparisons, non-overlapping periods are often easier to explain.
2015 to 2019
2020 to 2024
Before comparing vintages:
- Verify that the variable definition is comparable.
- Check whether geographic boundaries changed.
- Examine the margins of error.
- Remember that the estimates represent multi-year periods.
- Adjust dollar values when necessary for the question you are asking.
Do not mix inflation-adjusted dollar years without thinking
The 2024 ACS Data Profile labels median household income in 2024 inflation-adjusted dollars.
An older ACS vintage can report income in a different year's dollars.
If you compare income levels across vintages, check the dollar basis before interpreting the change.
Validate Census data before using it
A few simple checks catch many mistakes.
assert states["state"].str.len().eq(2).all()
assert not states["state"].duplicated().any()
valid_poverty = (
states["poverty_rate"]
.dropna()
.between(0, 100)
)
assert valid_poverty.all()
assert (
states["population"]
.dropna()
.ge(0)
.all()
)
assert (
counties_ca["geoid"]
.str.len()
.eq(5)
.all()
)
These checks do not prove that the analysis is correct. They help detect obvious data-processing problems.
Record metadata for reproducibility
| Item | Example |
|---|---|
| Vintage | 2024 |
| Dataset | acs/acs5/profile |
| Variables | DP05_0001E, DP03_0062E, etc. |
| Geography | state:* |
| Parent geography | state:06, if applicable |
| Retrieval date | Date the API was queried |
| Variable labels | Official Census descriptions |
| Transformation | Numeric cleaning, rankings, calculations |
metadata = {
"vintage": 2024,
"dataset": "acs/acs5/profile",
"geography": "state:*",
"retrieved": "YYYY-MM-DD",
"variables": VARIABLES,
}
Save the cleaned data
states.to_csv(
"census_acs_2024_states.csv",
index=False,
)
counties_ca.to_csv(
"census_acs_2024_california_counties.csv",
index=False,
)
Common Census API errors
Missing API key
import os
print(
bool(
os.getenv(
"CENSUS_API_KEY"
)
)
)
Invalid API key
Make sure the entire key was copied correctly and that it has been activated if required.
Invalid variable
A variable may not exist in the selected vintage or dataset. Check the exact variable metadata page for that dataset.
More than 50 variables
A standard query supports up to 50 variables. Split a large request into smaller calls or use a supported group query where appropriate.
Geography error
Check whether the dataset supports the geography you requested.
County query returns an error
for=county:*&in=state:06
FIPS codes lost their leading zeros
Do not convert geography identifiers to integers. Keep them as strings.
Strange negative values
Check the dataset's annotation and special-value documentation before treating an unusual value as a real estimate.
Empty DataFrame
Check the API response itself before debugging pandas. The problem may be an invalid variable, unsupported geography, incorrect vintage, or API error.
Frequently asked questions
Do I need an API key for the Census API?
Yes. Current Census developer documentation requires an API key for data queries.
What is the Census API base URL?
https://api.census.gov/data/
How many variables can I request?
A standard Census Data API call can include up to 50 variables.
What does state:* mean?
It requests all available state-level areas for a dataset that supports state geography.
How do I request counties in one state?
for=county:*&in=state:06
Why should FIPS codes stay as strings?
Geographic identifiers often contain leading zeros. Converting them to integers can damage the identifier and break data joins.
What does E mean in an ACS variable?
For Data Profile variables, E represents an estimate.
What does M mean?
M represents the matching margin of error.
What do PE and PM mean?
PE is a percent estimate and PM is the percent margin of error.
What is the difference between ACS 1-year and ACS 5-year data?
ACS 1-year estimates use 12 months of data and focus on larger areas. ACS 5-year estimates combine 60 months and provide coverage for all areas.
Can I use Census API data to make maps?
Yes. The Data API provides statistics and geographic IDs. Detailed maps normally require boundaries from TIGER/Line files, TIGERweb, or another geographic source.
Can I compare ACS 5-year estimates across time?
Yes, but check the periods, variable definitions, geography changes, margins of error, and dollar basis.
Final workflow
- Choose the Census program and dataset.
- Choose the correct vintage.
- Find the official variable definitions.
- Request the matching margins of error.
- Get a Census API key.
- Start with a small test query.
- Convert the JSON response to pandas.
- Keep FIPS and GEOID values as strings.
- Convert measurement columns to numeric types.
- Check annotation and missing values.
- Validate geographic IDs and reasonable value ranges.
- Record the dataset, variables, geography, and retrieval date.
- Save the cleaned data.
- Build charts, maps, or models only after the data have been checked.
The main lesson is simple. Do not memorize Census variable codes. Do not guess geography syntax.
Use the API metadata for the exact dataset and vintage you are working with.
Once you understand variables, geography, FIPS codes, and margins of error, the Census API becomes much easier to use with Python.
Official references
- Census Data API User Guide
- ACS 5-year developer documentation
- 2024 ACS 5-year Data Profile variables
- 2024 ACS 5-year Data Profile geography
- Census guidance for ACS estimates
