cd /news/developer-tools/how-to-track-people-also-ask-results… · home topics developer-tools article
[ARTICLE · art-43164] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

How to Track People Also Ask Results with a SERP API

A developer built a Python script that uses a SERP API to track Google's People Also Ask (PAA) results over time. The script fetches PAA questions for a set of keywords, saves snapshots as CSV files, and enables comparison to detect new or disappearing questions. This approach helps SEO professionals and content researchers identify content gaps and shifts in search intent without relying on large SEO platforms.

read13 min views1 publishedJun 29, 2026

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.

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.

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:

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.

We will write a Python script that:

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.

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:

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 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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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:

For AI teams, they can become:

For SEO teams, they can become:

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.

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.

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.

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.

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.

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?

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

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.

If your audience is local or regional, test location-specific SERPs.

PAA can change across countries and cities.

If you only look at today's results, you cannot detect changes.

Save snapshots.

The value is in the history.

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.

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.

── more in #developer-tools 4 stories · sorted by recency
── more on @google 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-to-track-people-…] indexed:0 read:13min 2026-06-29 ·