FRED API Python Tutorial: Code, Charts, Animation, and Notebook
Learn how to download official economic data, clean it with pandas, create useful tables, build charts, animate results, and compare ALFRED revisions in one reproducible workflow.
FRED API Python Tutorial
Learn how to download official economic data, clean it with pandas, create useful tables, build charts, animate results, and compare ALFRED revisions in one reproducible workflow.
- FRED API request Series ID, API key, and dates
- pandas DataFrame Clean dates, values, and missing data
- Tables and charts Explain the economic result clearly
The FRED API lets you download U.S. economic data directly into Python. You can collect inflation, unemployment, interest rate, GDP, wage, housing, and financial data without copying numbers by hand. This tutorial gives you reusable code, a working animation, and a complete notebook.
What you will build
- Create and protect a FRED API key.
- Download one or several economic series.
- Read series units, frequency, and update dates.
- Convert results into clean pandas DataFrames.
- Calculate inflation and moving averages.
- Create summary tables and static charts.
- Build an animated economic chart.
- Compare historical ALFRED data vintages.
What is the FRED API?
FRED stands for Federal Reserve Economic Data. It is maintained by the Federal Reserve Bank of St. Louis. FRED brings together data from the Bureau of Labor Statistics, the Bureau of Economic Analysis, the Federal Reserve, and many other official sources.
The API is a web service. Your Python program sends a request to an API address. FRED returns the requested information in a structured format. JSON works well with Python because it can be converted into a pandas DataFrame with only a few steps.
Economic series used in this tutorial
| Series ID | Economic measure | Frequency | Units |
|---|---|---|---|
CPIAUCSL |
Consumer Price Index | Monthly | Index, seasonally adjusted |
UNRATE |
Unemployment rate | Monthly | Percent, seasonally adjusted |
FEDFUNDS |
Federal funds effective rate | Monthly | Percent, not seasonally adjusted |
GDPC1 |
Real gross domestic product | Quarterly | Billions of chained 2017 dollars |
These series cover prices, jobs, monetary policy, and economic production. Always check the current metadata before publishing a result.
How to get a FRED API key
Create a FRED account, request an API key, and keep that key outside public code. Do not place your real key inside a notebook that will be shared on GitHub.
macOS or Linux
export FRED_API_KEY="your_real_api_key"
Windows PowerShell
$env:FRED_API_KEY="your_real_api_key"
Read the key in Python
import os
api_key = os.getenv("FRED_API_KEY")
if not api_key:
raise RuntimeError("FRED_API_KEY is not set.")
Install the Python packages
python -m pip install pandas requests matplotlib plotly jupyter
| Package | Purpose |
|---|---|
| requests | Sends requests to the FRED API |
| pandas | Cleans, joins, transforms, and summarizes data |
| matplotlib | Creates static charts |
| plotly | Creates interactive and animated charts |
| jupyter | Runs the tutorial as a notebook |
Make your first FRED API request
The main endpoint used here is https://api.stlouisfed.org/fred/series/observations. The request needs a series ID, an API key, and an output format.
import os
import requests
url = "https://api.stlouisfed.org/fred/series/observations"
params = {
"series_id": "UNRATE",
"api_key": os.getenv("FRED_API_KEY"),
"file_type": "json",
"observation_start": "2020-01-01",
}
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
The observations section contains dates and values. Economic values often arrive as text, so convert them before doing calculations.
Convert FRED data into a pandas DataFrame
import pandas as pd
df = pd.DataFrame(data["observations"])
df["date"] = pd.to_datetime(
df["date"],
errors="coerce",
)
df["value"] = pd.to_numeric(
df["value"],
errors="coerce",
)
df = (
df[["date", "value"]]
.dropna(subset=["date"])
.set_index("date")
.sort_index()
)
df.head()
errors="coerce"? It converts invalid dates or values into missing values. This is safer than allowing text to enter a calculation.Create a reusable FRED API client
A reusable client reduces repeated code. It also adds a timeout, retries temporary server errors, and returns clearer error messages.
import os
from typing import Any
import pandas as pd
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
BASE_URL = "https://api.stlouisfed.org/fred"
def build_session() -> requests.Session:
retry = Retry(
total=4,
backoff_factor=1,
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=("GET",),
respect_retry_after_header=True,
)
session = requests.Session()
session.mount(
"https://",
HTTPAdapter(max_retries=retry),
)
return session
SESSION = build_session()
def fred_get(
endpoint: str,
params: dict[str, Any],
) -> dict[str, Any]:
api_key = os.getenv("FRED_API_KEY")
if not api_key:
raise RuntimeError(
"FRED_API_KEY is not set."
)
request_params = {
"api_key": api_key,
"file_type": "json",
**params,
}
response = SESSION.get(
f"{BASE_URL}/{endpoint}",
params=request_params,
timeout=30,
)
if not response.ok:
try:
details = response.json().get(
"error_message",
response.text,
)
except ValueError:
details = response.text
raise RuntimeError(
f"FRED API request failed with "
f"HTTP {response.status_code}: {details}"
)
return response.json()
Download a FRED series with Python
def get_series(
series_id: str,
observation_start: str = "2000-01-01",
observation_end: str | None = None,
units: str = "lin",
frequency: str | None = None,
aggregation_method: str = "avg",
realtime_start: str | None = None,
realtime_end: str | None = None,
) -> pd.DataFrame:
params: dict[str, Any] = {
"series_id": series_id,
"observation_start": observation_start,
"units": units,
"sort_order": "asc",
}
optional_params = {
"observation_end": observation_end,
"frequency": frequency,
"aggregation_method": (
aggregation_method if frequency else None
),
"realtime_start": realtime_start,
"realtime_end": realtime_end,
}
params.update({
key: value
for key, value in optional_params.items()
if value
})
payload = fred_get(
"series/observations",
params,
)
observations = payload.get(
"observations",
[],
)
if not observations:
return pd.DataFrame(
columns=[series_id],
index=pd.DatetimeIndex([], name="date"),
)
frame = pd.DataFrame(observations)
frame["date"] = pd.to_datetime(
frame["date"],
errors="coerce",
)
frame[series_id] = pd.to_numeric(
frame["value"],
errors="coerce",
)
return (
frame[["date", series_id]]
.dropna(subset=["date"])
.set_index("date")
.sort_index()
)
unemployment = get_series(
series_id="UNRATE",
observation_start="2010-01-01",
)
unemployment.tail()
Check a series before using it
A series ID is not enough. Confirm the title, frequency, units, seasonal adjustment, date range, and last update date.
def get_series_info(
series_id: str,
) -> dict[str, Any]:
payload = fred_get(
"series",
{"series_id": series_id},
)
series_list = payload.get(
"seriess",
[],
)
if not series_list:
raise ValueError(
f"No metadata returned for {series_id}."
)
return series_list[0]
cpi_info = get_series_info("CPIAUCSL")
Download several FRED series
SERIES = {
"CPIAUCSL": "CPI",
"UNRATE": "Unemployment Rate",
"FEDFUNDS": "Federal Funds Rate",
}
frames = [
get_series(
series_id,
observation_start="2000-01-01",
).rename(
columns={series_id: label}
)
for series_id, label in SERIES.items()
]
monthly = pd.concat(
frames,
axis=1,
).sort_index()
monthly.tail()
An outer join is used because one series may contain an observation for a date when another series does not. Do not fill every missing value without checking why it is missing.
Calculate inflation, moving averages, and a rebased index
Year over year inflation
monthly["Inflation YoY"] = (
monthly["CPI"]
.pct_change(
periods=12,
fill_method=None,
)
* 100
)
Three month moving average
monthly["CPI 3 Month Average"] = (
monthly["CPI"]
.rolling(window=3)
.mean()
)
Rebase CPI to 100
first_cpi = monthly["CPI"].dropna().iloc[0]
monthly["CPI Rebased"] = (
monthly["CPI"] / first_cpi
) * 100
Create a useful summary table
analysis = monthly[
[
"Inflation YoY",
"Unemployment Rate",
"Federal Funds Rate",
]
].dropna(how="all")
summary = analysis.agg(
["count", "mean", "min", "median", "max"]
).T
summary["latest"] = analysis.apply(
lambda series: series.dropna().iloc[-1]
)
summary["latest_date"] = [
analysis[column].last_valid_index().date()
for column in analysis.columns
]
summary.round(2)
| Indicator | Count | Mean | Minimum | Median | Maximum | Latest |
|---|---|---|---|---|---|---|
| Inflation YoY | Generated by code | Generated by code | Generated by code | Generated by code | Generated by code | Generated by code |
| Unemployment Rate | Generated by code | Generated by code | Generated by code | Generated by code | Generated by code | Generated by code |
| Federal Funds Rate | Generated by code | Generated by code | Generated by code | Generated by code | Generated by code | Generated by code |
Create a static Matplotlib chart
import matplotlib.pyplot as plt
plot_data = analysis[
["Inflation YoY", "Unemployment Rate"]
].dropna()
fig, ax = plt.subplots(figsize=(11, 6))
ax.plot(
plot_data.index,
plot_data["Inflation YoY"],
label="CPI inflation, year over year",
)
ax.plot(
plot_data.index,
plot_data["Unemployment Rate"],
label="Unemployment rate",
)
ax.axhline(0, linewidth=0.8)
ax.set_title("U.S. Inflation and Unemployment")
ax.set_xlabel("Date")
ax.set_ylabel("Percent")
ax.legend()
ax.grid(alpha=0.25)
fig.tight_layout()
fig.savefig(
"fred_inflation_unemployment.png",
dpi=160,
bbox_inches="tight",
)
plt.show()
Use the filename fred-api-python-inflation-unemployment.png. A useful alt description is “U.S. CPI inflation and unemployment rate downloaded with the FRED API in Python.”
Animated U.S. inflation and unemployment chart
The chart below is built with inline SVG and plain JavaScript. It works without a chart library. Use the controls to play, pause, or move between years.
Annual average inflation and unemployment

Plotly version for your notebook
import plotly.express as px
annual = (
analysis[
["Inflation YoY", "Unemployment Rate"]
]
.dropna()
.assign(year=lambda frame: frame.index.year)
.groupby("year", as_index=False)
.mean()
)
animated = annual.melt(
id_vars="year",
var_name="indicator",
value_name="percent",
)
upper_limit = max(
10,
animated["percent"].max() * 1.15,
)
fig = px.bar(
animated,
x="indicator",
y="percent",
animation_frame="year",
color="indicator",
range_y=[0, upper_limit],
text_auto=".1f",
title="Annual Average Inflation and Unemployment",
)
fig.update_layout(
xaxis_title="Indicator",
yaxis_title="Percent",
showlegend=False,
)
fig.write_html(
"fred_economy_animation.html",
include_plotlyjs="cdn",
)
fig.show()
Change the frequency of FRED data
quarterly_unemployment = get_series(
series_id="UNRATE",
observation_start="2000-01-01",
frequency="q",
aggregation_method="avg",
)
| Parameter | Meaning |
|---|---|
avg |
Average |
sum |
Sum |
eop |
End of period |
What is the difference between FRED and ALFRED?
FRED usually shows the latest available information. ALFRED lets you study what information was available at an earlier point in time. This matters because GDP, employment, income, and other measures can be revised after their first release.
Compare two data vintages
def get_vintage(
series_id: str,
vintage_date: str,
observation_start: str = "2018-01-01",
) -> pd.DataFrame:
return get_series(
series_id=series_id,
observation_start=observation_start,
realtime_start=vintage_date,
realtime_end=vintage_date,
).rename(
columns={series_id: vintage_date}
)
vintage_a = get_vintage(
"GDPC1",
"2023-07-27",
)
vintage_b = get_vintage(
"GDPC1",
"2024-07-25",
)
gdp_revisions = pd.concat(
[vintage_a, vintage_b],
axis=1,
)
gdp_revisions["revision"] = (
gdp_revisions["2024-07-25"]
- gdp_revisions["2023-07-27"]
)
A positive revision means the newer vintage reports a higher value. A negative revision means it reports a lower value. Choose vintage dates that match your research question.
Handle common FRED API errors
Missing API key
Store the key in your environment before starting Python. Restart the notebook kernel if the environment was changed after Jupyter started.
Invalid series ID
Copy the exact series ID from the official FRED page. Check the date range and metadata before changing the code.
Empty DataFrame
The chosen date range may contain no observations. A vintage date may also fall outside the available real time period.
Timeout or temporary server error
Use a timeout, retry only temporary errors, and cache downloaded data when possible.
Validate data before publishing
| Item | Example |
|---|---|
| Series ID | CPIAUCSL |
| Frequency | Monthly |
| Units | Index |
| Seasonal adjustment | Seasonally adjusted |
| Retrieval date | Date the notebook was run |
| Transformation | Percent change from one year ago |
| Vintage date | Current data or a named historical date |
assert monthly.index.is_monotonic_increasing
assert not monthly.index.duplicated().any()
assert monthly["CPI"].dropna().gt(0).all()
assert monthly["Unemployment Rate"].dropna().between(
0,
100,
).all()
Limitations
- The selected indicators do not describe every part of the U.S. economy.
- Different series have different sources and release schedules.
- Some economic observations are revised after publication.
- A visual relationship does not prove causation.
- Annual averages can hide changes within a year.
- Seasonally adjusted and unadjusted data should not be mixed without explanation.
- Moving averages can hide sudden changes.
- The latest observation may be incomplete or revised later.
Frequently asked questions
Is the FRED API free?
FRED provides API access through a registered API key. Review the current official terms before using it in a high volume or commercial application.
Can I use the FRED API without an API key?
The official Version 1 documentation states that web service requests require an API key.
What format should I request?
JSON is a practical choice for Python. The observations endpoint also supports XML, Excel, and compressed CSV output.
Can I download several series in one request?
A normal series observations request uses one series ID. A common Python method is to loop through a list of IDs and join the returned DataFrames.
Can I use FRED data for forecasting?
Yes. Use time-ordered validation. For a fair historical test, consider ALFRED vintages so the model only receives information that was available on each forecast date.
Related economic analysis ideas
- Calculate real wage growth with CPI and earnings data.
- Compare CPI and PCE inflation.
- Build a U.S. recession indicator dashboard.
- Analyze the yield curve before recessions.
- Compare unemployment across states.
- Study housing affordability with mortgage rates and income.
- Forecast inflation with time series validation.
- Compare initial and revised GDP estimates.
Official sources
- FRED API overview
- FRED API key documentation
- FRED series observations documentation
- FRED series metadata documentation
- FRED real-time period documentation
- FRED vintage date documentation
- Matplotlib plot documentation
- Plotly animation documentation
On this page
What you will build What is FRED? Economic series API key Packages First API request pandas DataFrame Reusable client Download a series Metadata Several series Transformations Summary table Static chart Animated chart Frequency changes ALFRED revisions Errors Validation Limitations Notebook FAQ SourcesBuilt for practical, reproducible analysis of U.S. economic data.
