3spread
All guides

See what a fund actually holds (N-PORT look-through)

Find a fund family by name, then pull its largest reported positions from Form N-PORT, with each position's weight in the fund. Python, on a free key.

The 3spread TeamJuly 14, 2026Fund portfolios dataset

A fund's marketing page tells you its top ten holdings, as of a date it chose, rounded to one decimal. Form N-PORT tells you everything it holds, monthly, position by position, because the SEC requires it.

That is the difference between reading a fact sheet and doing a look-through. If you want to know what you actually own when you buy an ETF, or which funds are crowded into the same name, N-PORT is the primary source.

Everything below runs on the free Community tier: every dataset, 10,000 requests per day (600 per minute), no credit card.

Setup

pip install py3spread
export THREESPREAD_API_KEY="sk_live_..."

Find the fund family

You rarely start with a CIK. You start with a name, so resolve it first:

import itertools
from decimal import Decimal
from py3spread import Client
 
SEARCH = "vanguard"
 
with Client() as client:
    entities = client.fund_portfolios.entities(search=SEARCH, limit=5)["data"]
    if not entities:
        print(f"no registrants matching {SEARCH!r}")
        raise SystemExit
 
    print(f"registrants matching {SEARCH!r}:")
    for e in entities:
        print(f"  cik {e['cik']:<10} {e['company_name']} ({e['filing_count']} filings)")

Every dataset has an entity index like this one. It exists because the whole game with SEC data is resolving a human name to a filer identity, and doing it wrong quietly poisons everything downstream.

Pull the largest positions

    registrant = entities[0]
    holdings = list(itertools.islice(
        client.fund_portfolios.iter_holdings(
            registrant_cik=registrant["cik"],
            sort="val_usd",
            order="desc",
        ),
        15,
    ))
 
    print(f"\nlargest positions reported by {registrant['company_name']}:")
    for h in holdings:
        value = Decimal(h["val_usd"])
        pct = Decimal(h["pct_val"]) if h.get("pct_val") else None
        pct_str = f" ({pct:.2f}% of fund)" if pct is not None else ""
        print(f"  {h['name']:<45} ${value / 10**6:,.1f}M{pct_str} [{h['series_name'] or h['series_id']}]")

Three things to know before you trust this output.

val_usd is the position's value and pct_val is its weight in the fund. The weight is the one you usually want: $500M means nothing until you know whether the fund is $1B or $500B.

series_name is doing quiet, important work. A registrant is a fund family, not a fund. Vanguard files one N-PORT covering many series, and each series is what a normal person calls "a fund". If you aggregate positions across a registrant without splitting by series, you will produce a portfolio that no investor actually holds. The fallback to series_id is there because the name is not always populated.

Decimal, not float. The API returns these as strings at full precision on purpose. Parsing them as floats introduces rounding on the way in, and it will show up as pennies of drift when you sum a portfolio.

Where to take it

Filter iter_holdings on a security rather than a registrant and you invert the question: which funds hold this name, and at what weight? That is the crowding measure, and it is the one that tells you who has to sell if the story breaks.

Pull two consecutive months for one series and diff them, and you have the fund's trades at position level, a month at a time, rather than the quarterly snapshot 13F gives you.

The dataset

Full endpoint reference and response schemas are on the fund portfolios dataset page.

Run this yourself

Everything here works on the free Community tier: every dataset, 10,000 requests per day (600 per minute), no credit card. Provision a key and paste the code in.