Every quarter, institutional investors managing over $100 million disclose their equity holdings in SEC Form 13F filings — and most analysts still download them manually. Python's sec-edgar-downloader library automates the extraction: pull filings, parse XML into structured data, export to CSV. What takes 30 minutes by hand takes 30 seconds with code.
Key Takeaways
- 13F filings disclose institutional equity holdings each quarter, filed within 45 days of quarter-end
- The
sec-edgar-downloaderlibrary automates downloads from SEC EDGAR without web scraping - Parse 13F XML files into DataFrames to analyze positions, track changes, or export holdings data
Before You Start
This guide assumes you understand basic Python syntax, have installed packages with pip, and know how DataFrames work. No SEC filing expertise required — the code handles parsing logic.
Form 13F filings disclose equity holdings at quarter-end. Institutions file within 45 days, so filings dated May 15 reflect March 31 positions. Data includes tickers, share counts, market values. Excludes: short positions, most derivatives, holdings under the $100 million threshold.
What You Need
- Python 3.8+ with pip
- Internet connection to access SEC servers
- CIK number of the institution you're tracking (Central Index Key — SEC's unique identifier)
- Text editor or IDE (VS Code, PyCharm, Jupyter)
- Command-line access to run scripts
Step 1: Install Required Libraries
Open terminal and run:
pip install sec-edgar-downloader pandas lxml
The sec-edgar-downloader queries SEC EDGAR using official endpoints. lxml parses XML structure. pandas organizes extracted data. Total install time: under 60 seconds on most systems.
Step 2: Find the CIK Number
Navigate to the SEC EDGAR company search page. Enter the institution's name — "Berkshire Hathaway", for example. Search results show the CIK: 0001067983 for Berkshire.
Copy the CIK with or without leading zeros (SEC APIs accept both). Major 13F filers: hedge funds, pension funds, investment advisors, asset managers — any institution crossing the $100 million reporting threshold.
Step 3: Write the Download Script
Create download_13f.py and add:
from sec_edgar_downloader import Downloader
dl = Downloader("YourCompanyName", "your.email@example.com")
dl.get("13F-HR", "1067983", limit=1)
Replace the company name and email with real credentials. The SEC requires user-agent identification under its fair access policy — automated requests without proper headers get rate-limited or blocked. limit=1 downloads only the most recent filing. Remove it to pull full history.
Run: python download_13f.py
The library creates sec-edgar-filings/1067983/13F-HR/ and saves the XML filing in a timestamped subdirectory. First download: usually 2-5 seconds depending on filing size.
Step 4: Parse XML into Structured Data
13F filings use XML with a specific SEC schema. Each holding lives in an <infoTable> element: issuer name, CUSIP, shares, market value. Add this parsing function:
import pandas as pd
from lxml import etree
def parse_13f(xml_path):
tree = etree.parse(xml_path)
root = tree.getroot()
ns = {'ns': root.nsmap[None]}
holdings = []
for info in root.findall('.//ns:infoTable', ns):
holdings.append({
'issuer': info.find('.//ns:nameOfIssuer', ns).text,
'cusip': info.find('.//ns:cusip', ns).text,
'shares': int(info.find('.//ns:shrsOrPrnAmt/ns:sshPrnamt', ns).text),
'value': int(info.find('.//ns:value', ns).text) * 1000
})
return pd.DataFrame(holdings)
The function uses XPath to extract four data points per holding. Note: value in 13F filings represents thousands of dollars — multiply by 1000 for actual amounts.
Step 5: Export to CSV
Add:
import os
filing_dir = "sec-edgar-filings/1067983/13F-HR/"
subdirs = [d for d in os.listdir(filing_dir) if os.path.isdir(os.path.join(filing_dir, d))]
latest = sorted(subdirs)[-1]
xml_file = os.path.join(filing_dir, latest, "primary-document.xml")
df = parse_13f(xml_file)
df = df.sort_values('value', ascending=False)
df.to_csv('holdings.csv', index=False)
print(f"Exported {len(df)} holdings to holdings.csv")
The script finds the most recent filing (subdirectories use datestamp names), parses the XML, sorts by market value descending, exports. You now have a spreadsheet showing every position the institution held on the filing date.
Common Problems
AttributeError: 'NoneType' object has no attribute 'text' — The XML structure diverged from expected schema. Some 13F filings use alternate field names or amended formats. Wrap each .text call: element.text if element is not None else None.
HTTP 403 Forbidden during download — SEC blocks requests without proper user-agent headers. Verify you provided a real company name and email in Downloader(). Generic or missing user-agents trigger rate limiting immediately.
Empty DataFrame or zero holdings — Confirm you're parsing primary-document.xml, not full-submission.txt. Some filings place holdings in infotable.xml instead. List files in the download directory and verify which contains <infoTable> elements.
Best Practices
- Cache filings locally — SEC rate-limits to 10 requests/second. Download once, reuse files.
- Track amendments — Institutions file 13F-HR/A to correct errors. Check filing dates: amendments supersede original filings for the same quarter.
- Map CUSIP to tickers — 13F filings use CUSIP identifiers, not ticker symbols. Maintain a CUSIP-to-ticker database or use a financial data API for translation. The SEC does not provide this mapping.
- Calculate quarter-over-quarter changes — Download consecutive quarters and compare share counts to identify new positions, exits, size changes. Single-quarter snapshots miss the trading activity.
- Filter by value thresholds — Large institutions hold hundreds of positions. Focus on holdings above $50 million or top 20 positions to surface conviction bets rather than index noise.
When Not to Use This
13F data lags by 45+ days minimum. You're looking at positions from the previous quarter. Institutions may have already exited disclosed holdings. For trading decisions requiring current data: use real-time ownership feeds from financial data providers, not quarterly filings.
13F excludes short positions, most options, and funds below $100 million. Smaller managers don't file at all. For comprehensive ownership analysis: institutional databases that aggregate multiple filing types, not just 13F.
The XML parsing code works for standard 13F-HR filings post-2013. Older filings often use plain text tables. Amendments with non-standard schemas may break the parser. Test on multiple quarters before production use.
FAQ
How do I download 13F filings for multiple institutions at once?
Loop through a list of CIK numbers with Downloader.get(). Add time.sleep(0.1) between requests to respect SEC rate limits. Store each institution's filings in separate subdirectories by CIK for organization.
Can I track holdings over time with this method?
Remove limit=1 to download all historical filings for a CIK. Parse each quarter and append to a master DataFrame with a date column. Calculate holding period returns, turnover rates, trend changes across reporting periods.
How do I speed up parsing for large filings?
Use lxml.iterparse() for streaming XML parsing instead of loading entire files into memory. For batch processing across hundreds of filings: parallelize with multiprocessing while respecting rate limits.
What is the difference between 13F-HR and 13F-HR/A?
13F-HR: standard quarterly report. 13F-HR/A: amendment correcting errors in a prior filing. If both exist for the same quarter, use the 13F-HR/A — it supersedes the original with corrected data.
The next constraint: data freshness. If you're building a real-time monitoring system, 13F filings won't deliver. But if you're analyzing institutional behavior patterns, conviction sizing, or sector rotation over quarters — this method scales. The question is whether 45-day-old data changes your decision framework. For most fundamental analysis, it doesn't.