People Also Ask results are easy to ignore.
They sit in the middle of Google results, looking like a small accordion of questions.
But for SEO, content research, and AI search workflows, they are useful.
They show what people are asking around a topic.
Not what a keyword tool thinks they might ask.
Not what your content calendar guessed three months ago.
Actual questions appearing inside search results.
If you track them over time, you can see:
new questions appearing
old questions disappearing
competitor topics expanding
content gaps
question patterns by location
changes in search intent
In this article, we will build a small Python script that tracks People Also Ask results with a SERP API.
The goal is simple:
keyword → SERP API → People Also Ask questions → save snapshot → compare changes
No giant SEO platform.
Just a practical script you can understand, run, and modify.
What are People Also Ask results?
People Also Ask, often shortened to PAA, is the block of related questions that appears on many Google results pages.
For example, a query like:
best project management software
might show questions such as:
What is the best project management tool?
Which project management software is easiest to use?
Is Trello better than Asana?
What do small teams use for project management?
These questions are useful because they reveal the searcher's next question.
That is the good stuff.
A keyword tells you what someone typed.
People Also Ask tells you what they may want to understand next.
Why track PAA results?
A single PAA snapshot is useful.
Tracking PAA over time is better.
If you collect PAA questions every day or every week, you can answer questions like:
Which questions keep appearing?
Which new questions appeared this week?
Which topics are growing?
Which questions should we answer in content?
Which questions should an AI assistant handle?
Which competitors appear around these questions?
This is useful for:
- content planning
- SEO research
- featured snippet research
- competitor monitoring
- topic clustering
- AI agent search context
- FAQ generation
- market research
PAA data is not a magic oracle.
But it is a useful window into search intent. A slightly noisy window, yes, but still better than guessing in a dark room with a spreadsheet candle.
What we will build
We will write a Python script that:
- Reads a list of keywords
- Calls a SERP API for each keyword
- Extracts People Also Ask questions
- Normalizes the response
- Saves results to CSV
- Saves snapshots by date
- Compares today's questions with a previous snapshot
- Finds new and removed questions
The output will look like this:
keyword,question,answer_snippet,source_title,source_url,date
best project management software,What is the best project management software?,...,example.com,2026-01-01
The exact API response shape depends on your SERP API provider.
Common field names include:
people_also_ask
related_questions
paa_results
questions
So we will write the parser defensively.
Install dependencies
Create a new folder and install the packages:
pip install requests python-dotenv pandas
We will use:
requests → call the API
python-dotenv → load API keys
pandas → save and compare CSV files
Create a .env file
Create a .env file:
SERP_API_KEY=your_api_key
SERP_API_URL=https://your-serp-api-endpoint.example.com/search
This article uses a generic SERP API request format.
Your provider may use different parameter names.
For example, some providers use:
q
query
engine
location
gl
hl
device
Always check your provider's docs and adjust the request function.
Create a keyword file
Create keywords.txt:
best project management software
crm software for small business
google search api
serp api
ai search agent
Use real keywords from your workflow.
Do not only test clean examples.
PAA gets interesting when queries are commercial, local, or messy.
Step 1: Call the SERP API
Create a file called track_paa.py.
import os
import requests
from dotenv import load_dotenv
load_dotenv()
SERP_API_KEY = os.getenv("SERP_API_KEY")
SERP_API_URL = os.getenv("SERP_API_URL")
def fetch_serp(query, location="United States", language="en"):
if not SERP_API_KEY:
raise ValueError("Missing SERP_API_KEY")
if not SERP_API_URL:
raise ValueError("Missing SERP_API_URL")
params = {
"api_key": SERP_API_KEY,
"engine": "google",
"q": query,
"location": location,
"language": language,
"output": "json",
}
response = requests.get(
SERP_API_URL,
params=params,
timeout=30,
)
response.raise_for_status()
return response.json()
This function returns raw SERP JSON.
Keep it separate from the parser.
When something breaks, you want to know whether the API call failed or your parser failed.
Debugging soup is bad. Debugging soup with no labels is worse.
Step 2: Load keywords
Add a helper to load keywords from a text file.
def load_keywords(filename="keywords.txt"):
with open(filename, "r", encoding="utf-8") as file:
return [
line.strip()
for line in file
if line.strip()
]
This lets you update keyword lists without touching code.
Step 3: Find the PAA block
Different APIs may return PAA data under different keys.
Let's support several possible shapes.
def get_paa_items(data):
possible_keys = [
"people_also_ask",
"related_questions",
"paa_results",
"questions",
]
for key in possible_keys:
value = data.get(key)
if isinstance(value, list):
return value
return []
This is intentionally simple.
You can expand it later if your provider nests PAA inside another object.
For example:
def get_paa_items(data):
possible_keys = [
"people_also_ask",
"related_questions",
"paa_results",
"questions",
]
for key in possible_keys:
value = data.get(key)
if isinstance(value, list):
return value
serp = data.get("serp", {})
for key in possible_keys:
value = serp.get(key)
if isinstance(value, list):
return value
return []
Use the version that matches your API response.
Step 4: Normalize PAA items
A PAA item may include a question, answer snippet, title, link, and sometimes more fields.
We will normalize each item into one stable format.
def normalize_paa_item(keyword, item, date_string):
question = (
item.get("question")
or item.get("title")
or item.get("query")
or ""
)
answer_snippet = (
item.get("snippet")
or item.get("answer")
or item.get("description")
or ""
)
source_title = (
item.get("source_title")
or item.get("title")
or ""
)
source_url = (
item.get("link")
or item.get("url")
or item.get("source_url")
or ""
)
return {
"date": date_string,
"keyword": keyword,
"question": clean_text(question),
"answer_snippet": clean_text(answer_snippet),
"source_title": clean_text(source_title),
"source_url": source_url,
}
Add a small text cleaner:
import re
def clean_text(value):
if not value:
return ""
if not isinstance(value, str):
value = str(value)
value = re.sub(r"\s+", " ", value)
return value.strip()
This removes weird spacing.
Small cleanup now saves annoying CSV goblins later.
Step 5: Remove weak rows
Sometimes the API may return an item without a real question.
Skip those.
def is_valid_paa_row(row):
if not row["question"]:
return False
if len(row["question"]) < 5:
return False
return True
For content research, the question is the core field.
If there is no question, the row is not useful.
Step 6: Deduplicate questions
The same question may appear more than once.
Deduplicate by keyword plus normalized question.
def normalize_question_key(question):
question = question.lower().strip()
question = re.sub(r"[^\w\s]", "", question)
question = re.sub(r"\s+", " ", question)
return question
def dedupe_paa_rows(rows):
seen = set()
unique_rows = []
for row in rows:
key = (
row["keyword"].lower().strip(),
normalize_question_key(row["question"]),
)
if key in seen:
continue
seen.add(key)
unique_rows.append(row)
return unique_rows
This makes comparison cleaner.
Without dedupe, your snapshot can get noisy fast.
Step 7: Fetch PAA for one keyword
Now combine the API call and parser.
from datetime import date
def fetch_paa_for_keyword(keyword, location="United States", language="en"):
date_string = date.today().isoformat()
data = fetch_serp(
query=keyword,
location=location,
language=language,
)
paa_items = get_paa_items(data)
rows = [
normalize_paa_item(keyword, item, date_string)
for item in paa_items
]
rows = [
row
for row in rows
if is_valid_paa_row(row)
]
return dedupe_paa_rows(rows)
Now one keyword gives us clean rows.
Step 8: Fetch all keywords
Add a loop for the full keyword list.
import time
def fetch_all_paa(keywords, location="United States", language="en", delay=1):
all_rows = []
for keyword in keywords:
print(f"Fetching PAA for: {keyword}")
try:
rows = fetch_paa_for_keyword(
keyword=keyword,
location=location,
language=language,
)
all_rows.extend(rows)
except Exception as exc:
print(f"Failed keyword: {keyword}")
print(f"Error: {exc}")
time.sleep(delay)
return all_rows
The delay is there to avoid hitting APIs too aggressively.
Respect rate limits.
A script that gets rate-limited in five seconds is not automation. It is a tiny cannon with billing attached.
Step 9: Save a daily snapshot
Now save the results as CSV.
import pandas as pd
def save_snapshot(rows):
today = date.today().isoformat()
filename = f"paa_snapshot_{today}.csv"
df = pd.DataFrame(rows)
df.to_csv(filename, index=False)
print(f"Saved snapshot: {filename}")
return filename
Every time you run the script, you get a file like:
paa_snapshot_2026-01-01.csv
That is useful for weekly or monthly comparison.
Full tracking script
Here is the full version.
import os
import re
import time
import requests
import pandas as pd
from datetime import date
from dotenv import load_dotenv
load_dotenv()
SERP_API_KEY = os.getenv("SERP_API_KEY")
SERP_API_URL = os.getenv("SERP_API_URL")
def clean_text(value):
if not value:
return ""
if not isinstance(value, str):
value = str(value)
value = re.sub(r"\s+", " ", value)
return value.strip()
def normalize_question_key(question):
question = question.lower().strip()
question = re.sub(r"[^\w\s]", "", question)
question = re.sub(r"\s+", " ", question)
return question
def fetch_serp(query, location="United States", language="en"):
if not SERP_API_KEY:
raise ValueError("Missing SERP_API_KEY")
if not SERP_API_URL:
raise ValueError("Missing SERP_API_URL")
params = {
"api_key": SERP_API_KEY,
"engine": "google",
"q": query,
"location": location,
"language": language,
"output": "json",
}
response = requests.get(
SERP_API_URL,
params=params,
timeout=30,
)
response.raise_for_status()
return response.json()
def load_keywords(filename="keywords.txt"):
with open(filename, "r", encoding="utf-8") as file:
return [
line.strip()
for line in file
if line.strip()
]
def get_paa_items(data):
possible_keys = [
"people_also_ask",
"related_questions",
"paa_results",
"questions",
]
for key in possible_keys:
value = data.get(key)
if isinstance(value, list):
return value
serp = data.get("serp", {})
if isinstance(serp, dict):
for key in possible_keys:
value = serp.get(key)
if isinstance(value, list):
return value
return []
def normalize_paa_item(keyword, item, date_string):
question = (
item.get("question")
or item.get("title")
or item.get("query")
or ""
)
answer_snippet = (
item.get("snippet")
or item.get("answer")
or item.get("description")
or ""
)
source_title = (
item.get("source_title")
or item.get("title")
or ""
)
source_url = (
item.get("link")
or item.get("url")
or item.get("source_url")
or ""
)
return {
"date": date_string,
"keyword": keyword,
"question": clean_text(question),
"answer_snippet": clean_text(answer_snippet),
"source_title": clean_text(source_title),
"source_url": source_url,
}
def is_valid_paa_row(row):
if not row["question"]:
return False
if len(row["question"]) < 5:
return False
return True
def dedupe_paa_rows(rows):
seen = set()
unique_rows = []
for row in rows:
key = (
row["keyword"].lower().strip(),
normalize_question_key(row["question"]),
)
if key in seen:
continue
seen.add(key)
unique_rows.append(row)
return unique_rows
def fetch_paa_for_keyword(keyword, location="United States", language="en"):
date_string = date.today().isoformat()
data = fetch_serp(
query=keyword,
location=location,
language=language,
)
paa_items = get_paa_items(data)
rows = [
normalize_paa_item(keyword, item, date_string)
for item in paa_items
]
rows = [
row
for row in rows
if is_valid_paa_row(row)
]
return dedupe_paa_rows(rows)
def fetch_all_paa(keywords, location="United States", language="en", delay=1):
all_rows = []
for keyword in keywords:
print(f"Fetching PAA for: {keyword}")
try:
rows = fetch_paa_for_keyword(
keyword=keyword,
location=location,
language=language,
)
all_rows.extend(rows)
except Exception as exc:
print(f"Failed keyword: {keyword}")
print(f"Error: {exc}")
time.sleep(delay)
return all_rows
def save_snapshot(rows):
today = date.today().isoformat()
filename = f"paa_snapshot_{today}.csv"
df = pd.DataFrame(rows)
df.to_csv(filename, index=False)
print(f"Saved snapshot: {filename}")
return filename
def main():
keywords = load_keywords("keywords.txt")
rows = fetch_all_paa(
keywords=keywords,
location="United States",
language="en",
delay=1,
)
save_snapshot(rows)
print(f"Collected {len(rows)} PAA rows.")
if __name__ == "__main__":
main()
Run it:
python track_paa.py
You should get a CSV snapshot.
Compare two snapshots
Tracking becomes useful when you compare snapshots.
Let's say you have:
paa_snapshot_2026-01-01.csv
paa_snapshot_2026-01-08.csv
Create a new file called compare_paa_snapshots.py.
import re
import pandas as pd
def normalize_question_key(question):
question = str(question).lower().strip()
question = re.sub(r"[^\w\s]", "", question)
question = re.sub(r"\s+", " ", question)
return question
def add_compare_key(df):
df = df.copy()
df["question_key"] = df["question"].apply(normalize_question_key)
df["compare_key"] = (
df["keyword"].str.lower().str.strip()
+ "||"
+ df["question_key"]
)
return df
def compare_snapshots(old_file, new_file):
old_df = pd.read_csv(old_file)
new_df = pd.read_csv(new_file)
old_df = add_compare_key(old_df)
new_df = add_compare_key(new_df)
old_keys = set(old_df["compare_key"])
new_keys = set(new_df["compare_key"])
added_keys = new_keys - old_keys
removed_keys = old_keys - new_keys
unchanged_keys = old_keys & new_keys
added = new_df[new_df["compare_key"].isin(added_keys)]
removed = old_df[old_df["compare_key"].isin(removed_keys)]
unchanged = new_df[new_df["compare_key"].isin(unchanged_keys)]
return added, removed, unchanged
def main():
old_file = "paa_snapshot_2026-01-01.csv"
new_file = "paa_snapshot_2026-01-08.csv"
added, removed, unchanged = compare_snapshots(old_file, new_file)
added.to_csv("paa_added.csv", index=False)
removed.to_csv("paa_removed.csv", index=False)
unchanged.to_csv("paa_unchanged.csv", index=False)
print(f"Added questions: {len(added)}")
print(f"Removed questions: {len(removed)}")
print(f"Unchanged questions: {len(unchanged)}")
if __name__ == "__main__":
main()
Run:
python compare_paa_snapshots.py
Now you get:
paa_added.csv
paa_removed.csv
paa_unchanged.csv
This is where the PAA tracking starts to become useful.
What to do with new PAA questions
New PAA questions can mean several things.
They may show:
new search intent
new concerns
new comparison angles
new competitor awareness
new product category language
new educational gaps
For content teams, new PAA questions can become:
- FAQ sections
- article headings
- comparison page ideas
- support docs
- glossary entries
- video scripts
- social post topics
For AI teams, they can become:
- evaluation questions
- agent test prompts
- retrieval queries
- topic expansion data
For SEO teams, they can become:
- keyword clusters
- on-page optimization ideas
- featured snippet targets
- content refresh signals
Not every PAA question deserves content.
Some are too broad.
Some are repetitive.
Some are weird little search mushrooms.
But repeated PAA questions are worth watching.
Group questions by keyword
You can quickly count how many PAA questions each keyword returns.
df = pd.read_csv("paa_snapshot_2026-01-08.csv")
summary = (
df.groupby("keyword")
.size()
.reset_index(name="paa_count")
.sort_values("paa_count", ascending=False)
)
summary.to_csv("paa_keyword_summary.csv", index=False)
print(summary)
This helps you find which topics have more question depth.
A keyword with many PAA results may be good for educational content.
A keyword with no PAA results may still matter, but it may be more direct or transactional.
Group questions by words
A simple word count can reveal repeated patterns.
from collections import Counter
def tokenize(text):
text = str(text).lower()
text = re.sub(r"[^\w\s]", "", text)
stopwords = {
"what", "how", "is", "are", "the", "a", "an",
"to", "for", "of", "and", "in", "with", "does",
}
words = [
word
for word in text.split()
if word not in stopwords and len(word) > 2
]
return words
df = pd.read_csv("paa_snapshot_2026-01-08.csv")
counter = Counter()
for question in df["question"]:
counter.update(tokenize(question))
print(counter.most_common(20))
This is basic, but useful.
If words like:
pricing
alternative
free
compare
best
tools
api
keep appearing, you know what users are circling around.
Search intent leaves fingerprints.
Add location tracking
People Also Ask can change by location.
If your SERP API supports location parameters, track them.
Modify rows to include location:
def normalize_paa_item(keyword, item, date_string, location):
question = (
item.get("question")
or item.get("title")
or item.get("query")
or ""
)
answer_snippet = (
item.get("snippet")
or item.get("answer")
or item.get("description")
or ""
)
source_title = (
item.get("source_title")
or item.get("title")
or ""
)
source_url = (
item.get("link")
or item.get("url")
or item.get("source_url")
or ""
)
return {
"date": date_string,
"location": location,
"keyword": keyword,
"question": clean_text(question),
"answer_snippet": clean_text(answer_snippet),
"source_title": clean_text(source_title),
"source_url": source_url,
}
Then your comparison key should include location:
df["compare_key"] = (
df["location"].str.lower().str.strip()
+ "||"
+ df["keyword"].str.lower().str.strip()
+ "||"
+ df["question_key"]
)
This is useful for local SEO and regional content planning.
Save to SQLite instead of CSV
CSV is fine at the beginning.
Once snapshots pile up, use SQLite.
import sqlite3
def save_to_sqlite(rows, database="paa_tracking.db"):
df = pd.DataFrame(rows)
with sqlite3.connect(database) as connection:
df.to_sql(
"paa_results",
connection,
if_exists="append",
index=False,
)
Then you can query historical data:
SELECT keyword, question, COUNT(*) as appearances
FROM paa_results
GROUP BY keyword, question
ORDER BY appearances DESC;
This helps find persistent questions.
Persistent questions are usually more valuable than one-day surprises.
A note on SERP API providers
Most SERP API providers can return some form of Google result data, but the response shapes differ.
When testing providers, check:
Does it return People Also Ask data?
What is the field name?
Does it include answer snippets?
Does it include source URLs?
Does location targeting work?
Are empty PAA blocks common?
Is the response easy to normalize?
Common mistakes
Only testing one keyword
One keyword proves very little.
Test at least 20 to 50 keywords.
Include:
commercial queries
informational queries
comparison queries
branded queries
local queries
long-tail queries
Treating every PAA question as content-worthy
Some questions are not worth a full article.
Some belong in a FAQ section.
Some belong inside an existing post.
Some are just noise wearing a question mark.
Ignoring location
If your audience is local or regional, test location-specific SERPs.
PAA can change across countries and cities.
Not saving snapshots
If you only look at today's results, you cannot detect changes.
Save snapshots.
The value is in the history.
Sending PAA data directly to an LLM without cleaning
If you use PAA data for AI workflows, clean it first.
Use a source-aware format:
Question [1]
Keyword: serp api
Question: What is a SERP API?
Source URL: https://example.com
Snippet: ...
Do not dump raw JSON into the prompt unless you enjoy token confetti.
Final thoughts
People Also Ask tracking is not complicated.
The basic workflow is:
keywords
→ SERP API
→ PAA questions
→ clean rows
→ daily snapshot
→ compare changes
The useful part is not one export.
The useful part is watching the questions change over time.
New questions can reveal fresh intent.
Repeated questions can reveal stable content opportunities.
Removed questions can show shifting SERP behavior.
For SEO, PAA tracking helps with content planning and search intent research.
For AI apps, it can help generate better test prompts and search-grounded answer flows.
Start small.
Use 20 keywords.
Collect once a week.
Save the CSV.
Compare snapshots.
Once the pattern is working, add locations, SQLite, alerts, and dashboards.
Search intent moves quietly. PAA tracking gives you a small radar.
Top comments (0)