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 with Python: A Practical pandas Tutorial
The FRED API makes it possible to download economic data directly into Python instead of manually exporting CSV files from the FRED website.
In this tutorial, you will build a reusable Python workflow for retrieving Federal Reserve Economic Data, converting observations into pandas DataFrames, checking series metadata, combining several indicators, calculating transformations such as inflation, and working with historical ALFRED vintages.
The examples use the official FRED API directly with requests, so you can see exactly what is sent to the API and what comes back.
Table of contents
- Quick FRED API example
- What is FRED?
- FRED API Version 1 vs Version 2
- Get a FRED API key
- Install Python packages
- Make your first request
- Convert observations to pandas
- Build a reusable function
- Check series metadata
- Download several series
- Calculate inflation
- Create a summary table
- Plot FRED data
- Change frequency
- FRED vs ALFRED
- Direct API vs fredapi
- Common errors
- Validate data
- Frequently asked questions
Quick example: download a FRED series with Python
If you already have a FRED API key, this is the shortest useful example:
import os
import requests
import pandas as pd
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()
observations = response.json()["observations"]
df = pd.DataFrame(observations)
df["date"] = pd.to_datetime(df["date"])
df["value"] = pd.to_numeric(df["value"], errors="coerce")
df = df[["date", "value"]].set_index("date")
print(df.tail())
This requests the U.S. unemployment rate from FRED and converts the observations into a pandas DataFrame indexed by date.
The official fred/series/observations endpoint supports parameters including the series ID, date range, frequency, transformations, real-time periods, and output format. See the official FRED observations documentation.
What is FRED?
FRED stands for Federal Reserve Economic Data. It is maintained by the Federal Reserve Bank of St. Louis and provides access to economic and financial time series from many government agencies and other data providers.
| Series ID | Indicator | Typical frequency |
|---|---|---|
CPIAUCSL |
Consumer Price Index | Monthly |
UNRATE |
Unemployment Rate | Monthly |
FEDFUNDS |
Effective Federal Funds Rate | Monthly |
GDPC1 |
Real Gross Domestic Product | Quarterly |
A FRED series ID uniquely identifies the data you want to retrieve. For example, UNRATE refers to the U.S. unemployment rate.
FRED API Version 1 vs Version 2
FRED currently documents two API versions.
Version 1 provides customizable, series-level access. It is useful when you want individual economic series, metadata, categories, releases, search results, or ALFRED real-time data.
Version 2 is designed primarily for retrieving observations for all series associated with a release in bulk, including historical observations.
This tutorial uses Version 1 because individual-series retrieval is the most common workflow for pandas analysis.
Reference: FRED API documentation.
Step 1: Get a FRED API key
FRED Version 1 web-service requests use a registered API key.
Do not hard-code your real API key inside a notebook that will be uploaded to GitHub or shared publicly. A better approach is to store it in an environment variable.
macOS or Linux
export FRED_API_KEY="your_api_key"
Windows PowerShell
$env:FRED_API_KEY="your_api_key"
Then read the value from Python:
import os
api_key = os.getenv("FRED_API_KEY")
if not api_key:
raise RuntimeError("FRED_API_KEY is not set.")
Step 2: Install the Python packages
python -m pip install pandas requests matplotlib
| Package | Purpose |
|---|---|
requests |
Sends HTTPS requests to FRED |
pandas |
Cleans, joins, transforms, and analyzes data |
matplotlib |
Creates charts |
You do not need a special FRED Python package to use the API.
Step 3: Make your first FRED API request
The observations endpoint is:
https://api.stlouisfed.org/fred/series/observations
Create the request:
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()
FRED returns a JSON object containing metadata about the request and an observations collection containing the individual values.
Step 4: Convert FRED observations to pandas
FRED observation values can arrive as text rather than numeric Python values. Convert them explicitly:
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()
)
print(df.tail())
Using errors="coerce" is useful because an invalid or missing numeric value becomes NaN rather than silently entering your analysis as text.
Step 5: Build a reusable FRED function
Repeating the entire request every time you need another indicator quickly becomes inconvenient. Create a reusable function instead:
import os
from typing import Any
import pandas as pd
import requests
BASE_URL = "https://api.stlouisfed.org/fred"
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 = requests.get(
f"{BASE_URL}/{endpoint}",
params=request_params,
timeout=30,
)
if not response.ok:
try:
error = response.json().get(
"error_message",
response.text,
)
except ValueError:
error = response.text
raise RuntimeError(
f"FRED request failed "
f"({response.status_code}): {error}"
)
return response.json()
Now create a function that returns a pandas DataFrame:
def get_series(
series_id: str,
observation_start: str = "2000-01-01",
observation_end: str | None = None,
) -> pd.DataFrame:
params = {
"series_id": series_id,
"observation_start": observation_start,
"sort_order": "asc",
}
if observation_end:
params["observation_end"] = observation_end
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()
)
Use it like this:
unemployment = get_series(
"UNRATE",
observation_start="2010-01-01",
)
print(unemployment.tail())
The same function can now retrieve thousands of other FRED series simply by changing the series ID.
Step 6: Check the series metadata
A technically valid API request does not guarantee that you selected the correct economic series.
FRED's fred/series endpoint returns metadata such as the title, frequency, units, seasonal adjustment, observation dates, and last update information. See the official series metadata documentation.
Create a helper:
def get_series_info(
series_id: str,
) -> dict:
payload = fred_get(
"series",
{"series_id": series_id},
)
series = payload.get("seriess", [])
if not series:
raise ValueError(
f"No metadata found for {series_id}"
)
return series[0]
Then:
info = get_series_info("CPIAUCSL")
for field in [
"title",
"frequency",
"units",
"seasonal_adjustment",
"observation_start",
"observation_end",
"last_updated",
]:
print(field, ":", info.get(field))
This step is especially important in research because two similarly named economic indicators may use different frequencies, definitions, adjustments, or sources.
Step 7: Download several FRED series
You can download multiple series individually and combine them by date.
SERIES = {
"CPIAUCSL": "CPI",
"UNRATE": "Unemployment Rate",
"FEDFUNDS": "Federal Funds Rate",
}
frames = []
for series_id, label in SERIES.items():
frame = get_series(
series_id,
observation_start="2000-01-01",
)
frame = frame.rename(
columns={series_id: label}
)
frames.append(frame)
monthly = pd.concat(
frames,
axis=1,
).sort_index()
print(monthly.tail())
pd.concat(..., axis=1) aligns the series using their date indexes.
The example intentionally uses an outer join. One series may contain an observation for a date when another does not.
Inspect missing values before deciding whether interpolation, forward filling, deletion, or another treatment is appropriate.
Step 8: Calculate year-over-year inflation
CPIAUCSL is an index level, not an inflation percentage.
A common year-over-year inflation calculation is:
monthly["Inflation YoY"] = (
monthly["CPI"]
.pct_change(
periods=12,
fill_method=None,
)
* 100
)
The calculation compares the CPI index with its value twelve months earlier.
Keep the original CPI column as well as the transformed series. This makes it easier to audit the analysis later.
Calculate a moving average
monthly["CPI 3M Average"] = (
monthly["CPI"]
.rolling(window=3)
.mean()
)
A moving average smooths short-term variation, but it also hides some abrupt changes. Whenever you publish a transformed economic series, state clearly that the values are calculated rather than original FRED observations.
Step 9: Create a summary table
Build an analysis DataFrame:
analysis = monthly[
[
"Inflation YoY",
"Unemployment Rate",
"Federal Funds Rate",
]
].dropna(how="all")
Then calculate descriptive statistics:
summary = analysis.agg(
[
"count",
"mean",
"min",
"median",
"max",
]
).T
summary["latest"] = analysis.apply(
lambda s: s.dropna().iloc[-1]
)
summary["latest_date"] = [
analysis[column]
.last_valid_index()
.date()
for column in analysis.columns
]
print(summary.round(2))
Do not publish placeholder values in the finished article.
Because FRED data are updated and some economic series are revised, the exact output depends on the retrieval date. For reproducible research, record the retrieval date alongside your results.
Step 10: Plot FRED data with Matplotlib
For example, compare inflation and unemployment:
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, YoY",
)
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()
plt.show()
When publishing the chart, use a descriptive filename such as fred-api-python-inflation-unemployment.png and meaningful alternative text describing what the image actually shows.
Step 11: Change the frequency
The observations endpoint can also aggregate higher-frequency data to a lower frequency.
params = {
"series_id": "UNRATE",
"frequency": "q",
"aggregation_method": "avg",
}
FRED documents three aggregation methods:
| Parameter | Meaning |
|---|---|
avg |
Average |
sum |
Sum |
eop |
End of period |
The aggregation parameter only has an effect when a frequency is specified.
Choose the aggregation method based on what the variable represents. Averaging is not automatically appropriate for every economic series.
FRED vs ALFRED: why data revisions matter
FRED is generally used to obtain the latest available values for an economic series.
ALFRED is useful when you want to know what information was available at a particular point in history.
This distinction matters because GDP, employment, income, and other indicators can be revised after their initial release.
FRED's API supports real-time periods and vintage dates, allowing researchers to reproduce historical information sets. See the series vintage dates documentation.
For example:
def get_vintage(
series_id: str,
vintage_date: str,
observation_start: str,
) -> pd.DataFrame:
payload = fred_get(
"series/observations",
{
"series_id": series_id,
"observation_start": (
observation_start
),
"realtime_start": vintage_date,
"realtime_end": vintage_date,
},
)
frame = pd.DataFrame(
payload["observations"]
)
frame["date"] = pd.to_datetime(
frame["date"]
)
frame[series_id] = pd.to_numeric(
frame["value"],
errors="coerce",
)
return (
frame[["date", series_id]]
.set_index("date")
)
You could then compare two vintages of real GDP:
old = get_vintage(
"GDPC1",
"2023-07-27",
"2018-01-01",
)
new = get_vintage(
"GDPC1",
"2024-07-25",
"2018-01-01",
)
comparison = pd.concat(
[
old.rename(
columns={"GDPC1": "old"}
),
new.rename(
columns={"GDPC1": "new"}
),
],
axis=1,
)
comparison["revision"] = (
comparison["new"]
- comparison["old"]
)
This is useful when backtesting forecasting models because using today's revised data to evaluate a historical forecast can introduce look-ahead bias.
Direct FRED API requests vs fredapi
Another option is the third-party Python package fredapi, which wraps the FRED web service and returns data in pandas-friendly formats. See fredapi on PyPI.
Use direct requests calls when:
- You want to understand the API itself.
- You need complete control over parameters.
- You want fewer package dependencies.
- You are building your own data pipeline.
Consider a wrapper when:
- You prefer a shorter interface.
- The wrapper already implements the features you need.
- You are comfortable depending on an additional package.
Neither method changes the underlying importance of verifying the series definition and metadata.
Common FRED API errors
API key is missing
import os
print(
bool(os.getenv("FRED_API_KEY"))
)
If you set the environment variable after launching Jupyter, restart the kernel.
Invalid series ID
Copy the exact series ID from FRED and retrieve its metadata before continuing.
Empty DataFrame
Possible causes include:
- The requested date range contains no observations.
- The series ID is incorrect.
- The historical vintage does not contain observations for that period.
- The API returned no observations.
HTTP error
response.raise_for_status()
For production applications, consider retrying temporary errors such as HTTP 429 or server-side 5xx errors with an appropriate backoff strategy.
Missing observations
Do not automatically replace missing economic observations with zero. Investigate the reason for the missing value first.
Validate FRED data before publishing
A simple validation checklist can prevent many research errors.
| Item | Example |
|---|---|
| Series ID | CPIAUCSL |
| Frequency | Monthly |
| Units | Index |
| Seasonal adjustment | Seasonally adjusted |
| Retrieval date | Date analysis was run |
| Transformation | Year-over-year percentage |
| Vintage | Current or named historical vintage |
You can also perform basic checks in Python:
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()
)
These checks do not prove that the analysis is economically correct, but they can reveal obvious data-processing errors.
Limitations to keep in mind
- Different economic series come from different underlying sources.
- Release schedules differ between indicators.
- Economic observations may be revised.
- Seasonally adjusted and unadjusted data are not interchangeable.
- A correlation between two FRED series does not prove causation.
- Aggregating monthly data can hide within-period movements.
- Moving averages smooth useful variation as well as noise.
- The latest observation may later be revised.
- Historical forecasting should consider the information actually available at the forecast date.
Frequently asked questions
Is a FRED API key required?
For the Version 1 web-service examples used in this tutorial, use a registered FRED API key.
What format should I use with Python?
JSON is convenient because requests you can parse it directly, and the observations can then be converted into a pandas DataFrame.
Can I download more than one FRED series?
A standard Version 1 series-observations request is centered on one series_id. A common Python workflow is therefore to retrieve the required series individually and join them by date.
For bulk release-level retrieval, also review FRED API Version 2.
Can I use FRED data for machine learning or forecasting?
Yes, but preserve chronological order during validation. When historical revisions matter, ALFRED vintages can help ensure your model only uses information that would actually have been available at the time.
Should I use requests or a Python FRED library?
Use requests if you want API transparency and parameter control. A wrapper such as fredapi can reduce boilerplate if its interface matches your project.
Final workflow
A reliable FRED workflow in Python is:
- Identify the correct FRED series.
- Check its metadata.
- Retrieve observations through the API.
- Convert dates and values explicitly.
- Preserve missing values until you understand them.
- Combine series by date.
- Document every transformation.
- Record the retrieval or vintage date.
- Validate the resulting dataset.
- Keep the analysis reproducible.
Once the API client is in place, switching from unemployment to inflation, GDP, interest rates, housing data, or thousands of other economic indicators usually requires little more than changing the series ID.
Official references
