Extract a 10-K section across multiple years
Pull the same 10-K item from five years of annual reports, then measure how much the text changed and which themes grew or faded.
Companies rarely rewrite their 10-K from scratch. Most of the text carries over, and the interesting part is what changed: a new risk, a dropped paragraph, a theme that suddenly shows up ten times. This tutorial pulls Item 1A (risk factors) from five years of Apple 10-Ks and compares them.
It uses two endpoints:
/v1/filingsto list a company's 10-K filings/v1/extractorto pull one item from each filing as plain text
The script
import os
import re
import requests
BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}
# 1. the last five 10-K filings for the company
r = requests.get(f"{BASE}/v1/filings", params={"query": 'ticker:AAPL AND formType:"10-K"', "size": 5},
headers=HEADERS, timeout=60)
filings = r.json()["filings"]
# 2. the same item from each one
years = {}
for f in reversed(filings): # oldest first
r = requests.get(f"{BASE}/v1/extractor", params={"url": f["linkToFilingDetails"], "item": "1A", "type": "text"},
headers=HEADERS, timeout=120)
r.raise_for_status()
years[f["filedAt"][:4]] = r.text
def paragraphs(text):
return {p.strip() for p in text.split("\n") if len(p.strip()) > 80}
# 3. size and change versus the year before
prev = None
print("filed words paragraphs new dropped")
for year, text in years.items():
paras = paragraphs(text)
new = len(paras - prev) if prev is not None else 0
dropped = len(prev - paras) if prev is not None else 0
print(f"{year} {len(text.split()):>6,} {len(paras):>9} {new:>4} {dropped:>7}")
prev = paras
# 4. how often a few themes come up each year
terms = ["tariff", "artificial intelligence", "pandemic", "interest rate"]
print("\n" + "term".ljust(24) + "".join(y.rjust(6) for y in years))
for t in terms:
print(t.ljust(24) + "".join(str(len(re.findall(t, txt, re.I))).rjust(6) for txt in years.values()))
Output
filed words paragraphs new dropped
2021 9,659 109 0 0
2022 10,221 112 35 32
2023 9,794 105 36 43
2024 9,913 108 35 32
2025 9,782 99 59 68
term 2021 2022 2023 2024 2025
tariff 5 5 2 2 10
artificial intelligence 0 0 1 3 6
pandemic 14 14 4 2 2
interest rate 2 2 2 2 2
Reading the result
- Size is stable, content is not. Item 1A stays between about 9,700 and 10,200 words every year, yet each year roughly a third of the paragraphs are new or reworded. Length alone hides that churn.
- 2025 was the biggest rewrite. 59 new paragraphs and 68 dropped ones, versus about 35 in earlier years. That is the year to read closely.
- Themes move. "Pandemic" falls from 14 mentions to 2 after 2022, "artificial intelligence" goes from 0 to 6, and "tariff" jumps from 2 to 10 in 2025.
A paragraph counts as "new" if its exact text did not appear the year before, so a single edited word makes a paragraph both new and dropped. For a softer comparison, use difflib.SequenceMatcher on paragraph pairs, or compare embeddings.
Variations
- Another item. Change
itemto7for MD&A,1for the business description or1Cfor cybersecurity (filed since fiscal 2023, so expect fewer years). - Another company or a peer group. Loop over tickers and keep
yearsper ticker. Filing search returns up to 200 rows per call. - More history. Raise
sizeto go further back. The extractor works on 10-Ks back to the early 2000s, though older filings use plain text documents and less consistent headings. - Amendments.
formType:"10-K"excludes10-K/A. Amended 10-Ks often contain only Part III, so they rarely matter for Item 1A.
Next steps
- Filing Section Extractor and its item codes.
- Extract 10-K risk factors for a single filing.
- Clean extracted sections for NLP before you embed or classify the text.
- Form 10-K dataset and API reference.