Pulling company financials three different ways

Comparing cash flow pulls from the SEC XBRL API, edgartools and yfinance

#### What this is

I was analysing free cash flows for the Magnificent 7 and kept running into things in SEC XBRL I did not properly understand. So I worked through it the way I usually do, asking Claude Cowork one question at a time, and asking it to test each answer against real filings and write the result to an HTML page I could read.

What follows is the organised version of that Q and A. The questions are mine. The testing, the numbers and the scripts are Claude Cowork's, run live in the session on 26 July 2026 rather than recalled from training. Every script is linked from the section it belongs to, and indexed at the end, so you can run any of it yourself.

**What this is not.** It is not a comprehensive comparison of these tools or platforms. The work sits on a handful of cash flow line items for seven companies over six quarters, which is what I happened to be working on, plus the basic XBRL tagging concepts I had to get straight along the way. I am sharing what I learnt on that narrow slice.

 [01Concept, tag and fact](#s1)
 [02The four routes, as slices of a cube](#s2)
 [03Why not use frames for everything](#s3)
 [04The taxonomy question](#s4)
 [05Saving filings to disk](#s5)
 [06R-pages, and what they look like](#s6)
 [07Standardisation](#s7)
 [08In-house versus outside money](#s8)
 [09edgartools, and what a statement is](#s9)
 [10Duration versus instant](#s10)
 [11yfinance, and the reconciliation](#s11)
 [12Which one to use with an AI agent](#s12)
 [13What else edgartools does](#s13)
 [14The skills, and reading them properly](#s14)
 [15Where to find it](#s15)
 [16Points to watch](#s16)
 [17Index of scripts and files](#s17)

Headline: the sources agree almost everywhere they overlap, and the single number that disagreed turned out to be an error in my own handling of a restatement, which no amount of internal checking would have caught. What separates the sources otherwise is **coverage, traceability and structure**, and on those they are not close.

## 01 Concept, tag and fact

**The question:** what is a concept, what is a tag, what is a fact? Is a concept a higher-level standardised thing that nobody can add to, with tags being the flexible layer underneath?

#### The correction: concept and tag are the same thing

There is no hierarchy between them. A **concept** IS a **tag**. The two words are used interchangeably, "concept" in the specification and "tag" in conversation. And companies absolutely can create their own. `tsla:GainLossOnDigitalAssets` is a concept Tesla invented and it sits at exactly the same level as `us-gaap:NetIncomeLoss`.

What *is* fixed is the **namespace**, the prefix before the colon. Nobody can add anything to `us-gaap:`. That namespace is controlled by FASB and published once a year. But any filer can declare its own namespace, which is what the `tsla:`, `amzn:`, `msft:` prefixes are, and put whatever it wants in there. So the standardised-versus-flexible intuition is right; it just lives in the prefix, not in a concept-versus-tag split.

#### The three things, defined

| Term | What it is | Analogy |
| --- | --- | --- |
| **Concept** also called a tag | A *definition* living in a taxonomy. It says: this is what "operating cash flow" means, it is measured in currency, it is a debit, it spans a period rather than a point in time. It holds no number. | A column definition in a schema |
| **Fact** | One reported *value*. A concept plus a company plus a period plus a unit plus the number, traceable to the filing it came from. | One cell in one row |
| **Taxonomy** | The dictionary of concepts, plus the relationships between them, what rolls into what and with what sign. | The whole schema |

#### A real fact, in full

Apple's operating cash flow for the December 2025 quarter, exactly as the API returns it. The number is only one of twelve fields:

```
concept        us-gaap:NetCashProvidedByUsedInOperatingActivities   <- the concept / tag
label          Net Cash Provided by (Used in) Operating Activities
value          53925000000                                          <- the number
unit           USD
period_start   2025-09-28                                           <- these two make it
period_end     2025-12-27                                           <- a specific period
period_type    duration
fiscal_year    2026
fiscal_period  Q1
form_type      10-Q
filing_date    2026-01-30
accession      0000320193-26-000006                                 <- traceable to the filing
```

So: **one concept, many facts.** Apple has reported that single concept **162 times**, because every quarter adds one and every 10-Q repeats the prior-year comparative:

```
2024-09-29 -> 2025-06-28     81,754 mm   10-Q  0000320193-25-000073
2024-09-29 -> 2025-09-27    111,482 mm   10-K  0000320193-25-000079
2025-09-28 -> 2025-12-27     53,925 mm   10-Q  0000320193-26-000006
2025-09-28 -> 2026-03-28     82,627 mm   10-Q  0000320193-26-000013
```

Look at the last two. Same concept, consecutive filings, but one is a 90-day period and the next is 181 days. That single fact is why de-cumulation work exists at all.

## 02 The four routes, as slices of a cube

**The question:** what does "company concept" actually mean? Can you pass multiple tags, periods, companies? How does it differ from company facts? The intuition was that these are different lenses on the same data, like different SELECT statements.

**That intuition is exactly right.** It is one dataset and the four endpoints are four fixed query shapes over it. Every fact sits in a cube with three axes: **company**, **concept**, **period**. Each endpoint pins some axes and lets the others vary.

| Endpoint | Company | Concept | Period | Shape |
| --- | --- | --- | --- | --- |
| companyconcept | ONE, fixed | ONE, fixed | ALL | A line through time |
| companyfacts | ONE, fixed | ALL | ALL | A whole slab, one company |
| frames | ALL | ONE, fixed | ONE, fixed | A line across companies |
| submissions | ONE, fixed | not facts at all, this is the filing index | Metadata |

So directly: **companyconcept is always one company and always one concept.** You cannot pass a list of either. Seven companies and two concepts meant fourteen calls. They are cheap, so that is fine, but it is fourteen calls.

And **companyfacts is not different data**, it is the same facts with the concept axis unpinned. One call returns every concept a company has ever reported. If you want six concepts for one company, companyfacts once beats companyconcept six times.

| Route | What one call returned, measured | Bytes | Sec |
| --- | --- | --- | --- |
| `companyconcept` | Apple OCF, 132 facts | 20,187 | 0.4 |
| `companyfacts` | Apple everything, 505 concepts | 3,748,682 | 0.6 |
| `frames` | OCF for CY2025Q1, **4,786 companies** | 735,445 | 0.4 |
| `submissions` | Apple's last 1,000 filings, with URLs | 164,394 | 0.3 |

## 03 Why not use frames for everything

**The question:** why not just use frames everywhere, and pull every company in one go? Or conversely, can you not pull 4,000 companies via companyconcept?

Second half first: **no.** companyconcept is hard-wired to one CIK. 4,000 companies means 4,000 calls. That is what frames exists for.

First half is the interesting one. **Frames coverage is severely uneven and it drops companies silently.** Operating cash flow across five consecutive quarters:

| Frame | Total filers returned | Mag 7 present | Missing |
| --- | --- | --- | --- |
| CY2025Q1 | 4,786 | 6 of 7 | Apple |
| CY2025Q2 | 342 | 1 of 7 | all but Amazon |
| CY2025Q3 | 401 | 2 of 7 | Apple, Alphabet, Nvidia, Meta, Tesla |
| CY2025Q4 | 330 | 2 of 7 | Alphabet, Amazon, Nvidia, Meta, Tesla |
| CY2026Q1 | 4,493 | 6 of 7 | Apple |

**4,786 filers in Q1 and 342 in Q2.** That is not an outage. It is the de-cumulation problem in a different costume. A frame wants a discrete three-month period. In Q1 a calendar-year filer's year-to-date *is* three months, so it fits. By Q2 the same filer reports six months, which is not a quarter, so it falls out. Only filers that explicitly tag discrete quarters survive into Q2 and Q3.

Apple is absent from every frame, because its quarters end in late September, December, March and June and it only ever tags cumulative periods.

#### Check coverage before relying on a frame

A frame returns a clean, well-formed response with 342 companies in it, and nothing in it signals that 4,400 are absent. A market screen built on frames without a coverage check per quarter will look tidy and be incomplete.

Contrast the two routes for the same question. Frames normalises and gives comparable discrete quarters. companyconcept gives whatever the filer actually reported, which for the same window is 6 months for Apple, 9 months for Microsoft and 12 months for Amazon:

```
FRAMES CY2025Q1                     COMPANYCONCEPT, same window
  Apple      ABSENT                   Apple      53,887  (6 months!)
  Microsoft  37,044  Jan-Mar          Microsoft  93,515  (9 months!)
  Alphabet   36,150  Jan-Mar          Alphabet   36,150  (3 months)
  Amazon     17,015  Jan-Mar          Amazon    113,903  (12 months!)
  Nvidia     27,414  Jan27-Apr27      Nvidia     27,414  (3 months)
  Meta       24,026  Jan-Mar          Meta       24,026  (3 months)
  Tesla       2,156  Jan-Mar          Tesla       2,156  (3 months)
```

**Neither column is wrong.** Frames is comparable but incomplete. companyconcept is complete but requires reading start and end on every fact.

Reproduce: [frames_coverage.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/frames_coverage.py)

## 04 The taxonomy question

**The question:** how does a model know the taxonomy? Is it learned, or does it read the taxonomy document, or infer it?

Three ways, in order of preference, and the third is the useful one.

**Recall.** The common us-gaap concepts are known without looking. Fast, and also where errors hide.

**Probing.** Nvidia is the example. `PaymentsToAcquirePropertyPlantAndEquipment` returns HTTP 200 for Nvidia, which looks like success, but its last data point ends 2020-07-26. Nvidia switched concepts. Only checking the date range catches it.

**Reading the taxonomy that ships with the filing.** Every filing carries four sidecar files: schema, labels, calculation, presentation. The **calculation linkbase** holds the filer's own arithmetic with signs. Extracted from Amazon's, verbatim:

```
us-gaap_NetCashProvidedByUsedInFinancingActivities
    - us-gaap_FinanceLeasePrincipalPayments
    - amzn_RepaymentsOfLongTermFinancingObligations
    - us-gaap_RepaymentsOfShortTermDebt
    + us-gaap_ProceedsFromShortTermDebt
    + us-gaap_ProceedsFromIssuanceOfLongTermDebt
    - us-gaap_RepaymentsOfLongTermDebt
```

Nothing needs to be memorised. And it is a validation asset: bucketed line items must sum to the filed subtotal, or something is wrong and you find out before your reader does.

The file itself: [AMZN_taxonomy_cal.xml](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/AMZN_taxonomy_cal.xml)

## 05 Saving filings to disk

Downloads run inside the sandbox, so they go **straight to disk without passing through the model's context at all**. Fetching a 1 MB filing through a model's context window is expensive and caps out; fetching it with Python in a sandbox costs nothing. 6.7 MB across 14 files at effectively zero context cost.

#### Apple, 10-Q filed 2026-05-01

| File | What it is | Bytes |
| --- | --- | --- |
| [AAPL_10Q_filing.html](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/AAPL_10Q_filing.html) | The full rendered filing. This is HTML, not PDF | 999,810 |
| [AAPL_xbrl_instance.xml](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/AAPL_xbrl_instance.xml) | Raw inline-XBRL source, the machine-readable original | 930,242 |
| [AAPL_taxonomy_lab.xml](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/AAPL_taxonomy_lab.xml) | Label linkbase, concept to human wording | 503,758 |
| [AAPL_taxonomy_pre.xml](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/AAPL_taxonomy_pre.xml) | Presentation linkbase, statement ordering | 319,585 |
| [AAPL_taxonomy_cal.xml](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/AAPL_taxonomy_cal.xml) | Calculation linkbase, the arithmetic | 85,682 |
| [AAPL_taxonomy_schema.xsd](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/AAPL_taxonomy_schema.xsd) | Schema, declares any custom concepts | 35,422 |
| [AAPL_cashflow_statement.csv](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/AAPL_cashflow_statement.csv) | Cash flow statement, spreadsheet-ready | 9,962 |

#### Amazon, 10-Q filed 2026-04-30

| File | What it is | Bytes |
| --- | --- | --- |
| [AMZN_10Q_filing.html](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/AMZN_10Q_filing.html) | The full rendered filing | 1,241,244 |
| [AMZN_xbrl_instance.xml](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/AMZN_xbrl_instance.xml) | Raw inline-XBRL source | 1,255,723 |
| [AMZN_taxonomy_lab.xml](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/AMZN_taxonomy_lab.xml) | Label linkbase | 801,101 |
| [AMZN_taxonomy_pre.xml](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/AMZN_taxonomy_pre.xml) | Presentation linkbase | 536,368 |
| [AMZN_taxonomy_cal.xml](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/AMZN_taxonomy_cal.xml) | Calculation linkbase | 148,073 |
| [AMZN_taxonomy_schema.xsd](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/AMZN_taxonomy_schema.xsd) | Schema | 64,382 |
| [AMZN_cashflow_statement.csv](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/AMZN_cashflow_statement.csv) | Cash flow statement CSV | 10,813 |

**On format:** the filing document is HTML, not PDF. The SEC does not accept PDF as a primary filing format for these forms. Everything is HTML with the XBRL tags embedded inline in the same file. That is what "inline XBRL" means, one document that is simultaneously human-readable and machine-readable.

## 06 R-pages, and what they look like

**The question:** what are these R-pages, how do they look, can they be downloaded? They sound more digestible.

They are, and they are the most underused thing on EDGAR. When a filing is submitted the SEC's renderer splits it into **one small HTML page per statement and per note**, numbered R1, R2, R3, with an index file listing what is where. The cash flow statement comes out as a clean self-contained table, roughly 30 KB instead of the 1.2 MB full filing:

| Apple, 10-Q 2026-05-01 | Amazon, 10-Q 2026-04-30 |
| --- | --- |
| [R7 Cash Flows](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/R_pages/R7_CONDENSED_CONSOLIDATED_STATEMENTS_OF_CASH_FLOWS.html) [R2 Operations](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/R_pages/R2_CONDENSED_CONSOLIDATED_STATEMENTS_OF_OPERATIONS.html) [R4 Balance Sheet](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/R_pages/R4_CONDENSED_CONSOLIDATED_BALANCE_SHEETS_Unaudited.html) [R6 Shareholders Equity](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/R_pages/R6_CONDENSED_CONSOLIDATED_STATEMENTS_OF_SHAREHOLDER.html) [R9 Revenue note](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/R_pages/R9_Revenue.html) [R1 Cover Page](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/R_pages/R1_Cover_Page.html) [FilingSummary.xml (the index)](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AAPL_10Q_2026-05-01/R_pages/FilingSummary.xml) | [R2 Cash Flows](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/R_pages/R2_Consolidated_Statements_of_Cash_Flows.html) [R3 Operations](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/R_pages/R3_Consolidated_Statements_of_Operations.html) [R6 Balance Sheet](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/R_pages/R6_Consolidated_Balance_Sheets.html) [R9 Financial Instruments](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/R_pages/R9_Financial_Instruments.html) [R10 Leases note](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/R_pages/R10_Leases.html) [R1 Cover](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/R_pages/R1_Cover.html) [FilingSummary.xml (the index)](https://www.tigzig.com/story/sec-xbrl-three-ways/filings/AMZN_10Q_2026-04-30/R_pages/FilingSummary.xml) |

**The numbering is not standard across companies.** Apple's cash flow statement is R7; Amazon's is R2. You have to read FilingSummary.xml to know which is which. For spot-checking one number against the filed statement, R-pages are the right tool: 30 KB instead of 1 MB.

Reproduce: [download_rpages.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/download_rpages.py)

## 07 Standardisation

**The question:** do subtotals use the same tag everywhere, or can anyone call it something different? And are line items free-form text you have to categorise by hand?

### Subtotals: fully standard

All seven companies use the identical us-gaap concept for all three cash flow section totals. No ambiguity, no judgment call.

### Line items: tagged, not free text

Every line item carries a real concept. What is free-form is the **label**, and they are separate fields:

```
concept : us-gaap:PaymentsToAcquirePropertyPlantAndEquipment        <- standard, machine-readable
label   : "Payments for acquisition of property, plant and equipment"  <- free-form, Apple's wording
```

So you never categorise by reading English. You match on concept. The catch is custom extensions:

| Company | Line items | Custom concepts | Standard us-gaap | Subtotals standard |
| --- | --- | --- | --- | --- |
| Apple | 29 | 0 | 100.0% | yes |
| Alphabet | 44 | 2 | 95.5% | yes |
| Amazon | 30 | 2 | 93.3% | yes |
| Microsoft | 34 | 4 | 88.2% | yes |
| Nvidia | 34 | 4 | 88.2% | yes |
| Meta | 42 | 6 | 85.7% | yes |
| Tesla | 41 | 6 | 85.4% | yes |

24 distinct custom concepts across the seven, and they are where each company's idiosyncrasy lives: `tsla:GainLossOnDigitalAssets`, `tsla:IncreaseDecreaseInOperatingLeaseVehicles`, `nvda:PaymentsToAcquireNonMarketableSecurities`, `meta:PaymentsForHeldForSaleAssets`.

#### Two traps found by testing, not by reasoning

**Same economics, different concept.** Capex is not one concept. Five of these companies use `PaymentsToAcquirePropertyPlantAndEquipment`. Amazon and Nvidia use `PaymentsToAcquireProductiveAssets`. Query only the first and two of the seven silently return nothing.

**Section order is not standard.** Microsoft presents financing *before* investing. A first pass that detected sections by row position produced nonsense for six of seven companies. Anchoring on the subtotal concepts fixed it.

Reproduce: [tag_standardization.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/tag_standardization.py)

## 08 In-house versus outside money

**The question:** to split financing into internally generated versus externally raised money, is there a standard tag, or do you read each item and judge?

Better than feared. The financing section is almost entirely standard concepts and they already carry the distinction. Amazon Q1 2026, from the filing:

| Concept | Label as filed | $mm |
| --- | --- | --- |
| `us-gaap:ProceedsFromIssuanceOfLongTermDebt` | Proceeds from long-term debt | 53,441 |
| `us-gaap:ProceedsFromShortTermDebt` | Proceeds from short-term debt, and other | 6,018 |
| `us-gaap:RepaymentsOfShortTermDebt` | Repayments of short-term debt, and other | -6,109 |
| `us-gaap:FinanceLeasePrincipalPayments` | Principal repayments of finance leases | -468 |
| `amzn:RepaymentsOfLongTermFinancingObligations` | Principal repayments of financing obligations | -115 |
| **Net cash from financing activities** | **52,767** |

One custom concept out of five. The judgment is not *what is this line*, it is *which bucket does it belong in*, which is a modelling decision you would make regardless.

And note what it already says. Amazon's free cash flow in Q1 2026 was negative 18,171. In the same quarter it raised 53,441 of new long-term debt.

## 09 edgartools, and what a statement is

**The question:** when a library gives you "the statement" rather than facts, does that mean the machine-readable equivalent of the organised, hierarchical statement you would read in a filing? And without it, would you have to pull concepts one by one?

**Yes to both.**

This section covers edgartools for the narrow job of financial statements. For the rest of what it does, the Claude skills it ships, and where to find it, see sections [13](#s13), [14](#s14) and [15](#s15).

The raw API gives a bag of facts with no structure. You know Apple reported 82,627 for a concept. You do not know it belongs to the operating section, or that it is the subtotal rather than a line item, or what sits under it, or in what order. To build a statement from the raw API you would pull each concept individually and reconstruct the hierarchy yourself from the presentation linkbase.

edgartools does that reconstruction. `xbrl().statements.cashflow_statement()` returns the statement as a dataframe: every row in filed order, with its concept, label, indent level, parent and value.

### It also handles de-cumulation

This is not obvious from the surface API and it is worth knowing about, since the year-to-date subtraction is otherwise the fiddliest part of the job. `TTMCalculator.quarterize()` does the arithmetic for you: Q2 = YTD_6M minus Q1, Q3 = YTD_9M minus YTD_6M, Q4 = FY minus YTD_9M.

Tested against a hand-rolled implementation over 42 company-quarters of operating cash flow and capex, **41 matched exactly**. The one variance is the most useful thing on this page, so it is worth walking through.

#### The one number where there was a variance

Tesla's Q4 2024 capex came out as 2,783 by hand and 2,786 from the library. Three million dollars on a figure of roughly 2.8 billion, which is the kind of gap that is easy to dismiss. It was worth following up.

Tesla restates that line. From the FY2025 10-K onward it folded a separate solar-energy-systems line into property and equipment, which moved the historical figures. Every affected period exists in EDGAR twice:

```
period                 as originally filed        as restated
2024 nine months            8,556                    8,562
2024 full year             11,339                   11,342
```

Q4 is the full year minus the nine months, so the answer depends entirely on which vintage you subtract:

```
11,339 - 8,556  =  2,783    original basis      <- the hand-rolled version
11,342 - 8,556  =  2,786    MIXED basis          <- what the library produced
11,342 - 8,562  =  2,780    restated basis       <- correct
```

#### Neither of the first two is the figure you want

The hand-rolled version used original-basis figures throughout. Internally consistent, but it leaves Q4 2024 on a solar-exclusive basis while every later quarter in the same series is solar-inclusive. That is a definitional break inside a six-quarter comparison, and it does not look like an error on the page.

The library subtracted a restated annual figure from an original nine-month figure, so two vintages sat inside one subtraction.

**The correct figure is 2,780**, taking the latest filed value for both terms. A restated number is the company's current statement of the fact, and there is no good reason to carry a superseded one. The only exception is genuine point-in-time work where you deliberately want what was knowable on a given date, and that is a narrow, explicit choice rather than a default.

A restatement audit across all seven companies and both concepts found the policy changes **exactly one cell in 84**. Small in size, and still worth getting right, because the effect is on the comparability of the series rather than on the magnitude of any single number.

#### Why more than one source is good practice

Two independent paths had already agreed on 84 out of 84 cells before this came up. They agreed because they shared the same dedup policy, not because the policy was right. **Agreement between paths that share an assumption tells you nothing about that assumption.** It took a third path, using a different policy, for the difference to show.

Reconciling across sources is routine validation in finance rather than anything new.

A nice touch: quarterized facts are labelled with a data-quality flag, `HIGH` where the filer tagged the quarter directly and `MEDIUM` where the value was derived by subtraction. That distinction is exactly the thing you want surfaced rather than hidden.

```
     start        end   val_mm     quality
2024-09-29 2024-12-28   29,935     HIGH      <- as filed
2024-12-29 2025-03-29   23,952     MEDIUM    <- derived
2025-03-30 2025-06-28   27,867     MEDIUM    <- derived
2025-06-29 2025-09-27   29,728     MEDIUM    <- derived
2025-09-28 2025-12-27   53,925     HIGH      <- as filed
2025-12-28 2026-03-28   28,702     MEDIUM    <- derived
```

### On the standardisation layer

Every row carries the filer's actual concept AND edgartools' normalised name, side by side:

```
concept          us-gaap_PaymentsToAcquirePropertyPlantAndEquipment
label            Payments for acquisition of property, plant and equipment
standard_concept CapitalExpenses
```

The normalisation is a convenience layer, and like any general mapping it will sometimes name things differently from how your model wants them named. On Apple, for instance, `us-gaap:PaymentsOfDividends` maps to a standard concept called `DistributionsToMinorityInterests`, which is not how most analysts would bucket an ordinary dividend. That is not really a defect so much as the nature of a one-size mapping meeting a specific analytical question.

The sensible way to use it: treat `standard_concept` as a fast default, and where your analysis has its own definitions, key off the raw `concept` and apply your own bucket map. The library supports this properly rather than fighting it. The mapping lives in editable JSON (`concept_mappings.json`), there is a `MappingStore` that accepts a custom source, a per-company override directory, and an unmapped-concept logger so you can see what fell through. So you can extend it, override it, or bypass it entirely, and all three are first-class.

Reproduce: [edgartools_pull.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/edgartools_pull.py), [quarterize_test.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/quarterize_test.py) and [restatement_audit.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/restatement_audit.py)

**More on edgartools:** this section only used it for financial statements. [Section 13](#s13) covers the nine other capabilities tested here, from full-text search to segment-level dimensional facts. [Section 14](#s14) covers the Claude skills it ships and the three places where reading them changed this write-up. [Section 15](#s15) has the repository, documentation and install links.

## 10 Duration versus instant

**The question:** a duration in days, what level is it attached to? Statement level? And a balance sheet item has no duration, only a date. Is that right?

**Both correct.** It attaches to the **individual fact**, not the statement, and balance sheet items genuinely have no duration. Measured on Apple:

| Concept | Statement | period_type across all its facts |
| --- | --- | --- |
| `NetCashProvidedByUsedInOperatingActivities` | Cash flow | duration, 162 of 162 |
| `PaymentsToAcquirePropertyPlantAndEquipment` | Cash flow | duration, 103 of 103 |
| `RevenueFromContractWithCustomer...` | Income | duration, 113 of 113 |
| `CashAndCashEquivalentsAtCarryingValue` | Balance sheet | **instant, 237 of 237** |
| `AssetsCurrent` | Balance sheet | **instant, 284 of 284** |
| `StockholdersEquity` | Balance sheet | **instant, 407 of 407** |

An instant fact has **no period_start at all**, just a single date. So the cumulation problem does not exist on the balance sheet. Cash at 28 March 2026 is cash at 28 March 2026. Balance sheet extraction is genuinely easy; only the flow statements need de-cumulation.

And it is a property of the concept's *definition*, not of the statement it appears on. The taxonomy declares each concept as duration or instant, so a filer cannot report a balance sheet item as a duration even if it wanted to.

#### The detail worth knowing, found while testing this

Within Apple's *same* 10-Q, revenue is tagged as a **90-day discrete quarter** while operating cash flow is tagged as a **181-day cumulative** period. Same filing, same company, two conventions on two statements. This is why income statement work is easy and cash flow work is not.

Reproduce: [fact_anatomy.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/fact_anatomy.py)

## 11 yfinance, and the reconciliation

**The question:** pull the same cash flows from yfinance. What do the numbers say against SEC and edgartools? Is there a six-quarter cap, and do they pre-digest the data?

`pip install yfinance`, version **1.5.2**. Seven companies pulled in **2.1 seconds total**. It works, it is fast, and it is by far the least code of the three.

### The numbers: exact, everywhere they exist

Of the 42 company-quarters in the test window, yfinance had 33. On all 33, **both operating cash flow and capex matched the SEC figures to the dollar.** Set alongside the other reconciliations:

```
SEC raw API  vs  edgartools entity facts  :  84 cells,      0 mismatches
SEC raw API  vs  edgartools quarterize()  :  42 quarters,   41 identical, 1 exposed a real error
SEC raw API  vs  yfinance                 :  33 present,     0 differing,  9 MISSING
```

So data quality is not what separates them. All three derive from the same filings and produce the same numbers.

### The problem is coverage

The common belief is that yfinance caps quarterly data at six quarters. It is close, but it is not a fixed six, and that is the part worth knowing. It is a **rolling window that varies by company and drops the oldest quarter without saying so**:

| Company | Quarters returned | Oldest available | Newest available | Missing from the test window |
| --- | --- | --- | --- | --- |
| Apple | 6 | 2024-12-31 | 2026-03-31 | Q4 2024 |
| Microsoft | 6 | 2024-12-31 | 2026-03-31 | Q4 2024 |
| Alphabet | 5 | 2025-06-30 | 2026-06-30 | Q4 2024, Q1 2025 |
| Amazon | 6 | 2024-12-31 | 2026-03-31 | Q4 2024 |
| Nvidia | 7 | 2024-10-31 | 2026-04-30 | Q4 2024 |
| Meta | 6 | 2024-12-31 | 2026-03-31 | Q4 2024 |
| Tesla | 7 | 2024-12-31 | 2026-06-30 | Q4 2024, Q1 2025 |

#### A six-quarter grid built a quarter ago cannot be rebuilt from yfinance today

All seven have already dropped Q4 2024, and Alphabet and Tesla have lost Q1 2025 as well. That window was fully available three months ago. It is gone and it does not come back.

Alphabet simply returns five columns. Nothing in the response says two quarters used to be there. This is the same silent-incompleteness shape as the frames problem in section 03, and it is the failure mode that most reliably produces a confident wrong answer.

### What "pre-digestion" actually means, measured

| What it does | Detail | Verdict |
| --- | --- | --- |
| **De-cumulates for you** | Returns discrete quarters. No year-to-date subtraction needed. | real advantage |
| **Computes free cash flow** | A `Free Cash Flow` row, equal to OCF plus capex exactly. Checked on Apple, Amazon and Microsoft. | convenient |
| **Normalises the concepts** | Its own vocabulary: `Operating Cash Flow`, `Purchase Of PPE`. No us-gaap concepts anywhere. Handles the Amazon and Nvidia capex-concept difference invisibly. | helpful but opaque |
| **Normalises the dates** | Apple's period ending **2025-12-27** is labelled **2025-12-31**. Nvidia's **2026-01-25** becomes **2026-01-31**. Microsoft, which genuinely ends on month-end, matches. | convenient, slightly false |
| **Flattens the hierarchy** | A plain list of row labels. No level, no parent, no section. It also emits duplicate subtotals: `Operating Cash Flow` and `Cash Flow From Continuing Operating Activities` both appear. | loses structure |
| **Drops the audit trail** | No accession number, no form type, no filing date. You cannot trace a figure back to the document it came from. | disqualifying for publication |

On vocabulary consistency: across the seven companies there are **71 distinct row labels but only 30 appear in all seven**. The normalisation is real at the top, since the core subtotals are everywhere, but it thins out fast further down the statement, which is precisely where a line-item drilldown lives.

### On shared-IP throttling

A fair concern when many users share an outbound IP, so it was tested. 25 tickers pulled back to back: **25 returned data, 0 empty, 0 errors, 10.9 seconds, 0.43s per ticker.** No throttling at that volume. But this reads an endpoint Yahoo does not document or support, and there is no status page to check when it changes. SEC by contrast asks for an identifying User-Agent and publishes a rate limit, which is a relationship rather than a tolerance.

Annual data is also capped at **five years**. SEC carries Apple's operating cash flow back to fiscal 2007.

Reproduce: [yfinance_pull.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/yfinance_pull.py) and [compare3.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/compare3.py)

## 12 Which one to use with an AI agent

**The question:** for someone working with an AI agent rather than by hand, which source is the better default?

**edgartools as the primary, with the SEC XBRL API as the validation layer.** Not because the data is better, since on the overlap it is identical, but because of what each one gives you alongside the numbers.

yfinance is scoped for speed rather than depth, and that is what it is for. The window is short, five to seven quarters, and the response does not carry a filing reference, so a gap in coverage is not visible on the face of it. Alphabet comes back with five columns and nothing marks the two that are no longer there. For a quick look while you are still working out the question, that is a fair trade.

The SEC route carries the things you need when the answer has to hold up. A concept that does not exist returns nothing rather than something plausible. Every period carries its own start and end dates, so a mismatch between two figures is visible. And every figure carries an accession number, so where a number came from always has an answer.

| Consideration | SEC + edgartools | yfinance |
| --- | --- | --- |
| Numerical accuracy on overlap | exact | exact |
| Quarterly history depth | full XBRL era | 5 to 7 quarters, rolling |
| Annual history depth | to 2007 for Apple | 5 years |
| Reproducible next month | yes, filings are immutable | no, the window rolls |
| Traceable to a document | accession number on every fact | none |
| Line-item detail and hierarchy | full, with sections | flat label list |
| True fiscal period dates | as filed | normalised to month-end |
| De-cumulation | built in, with quality flags | done for you |
| Speed and code volume | more calls, more logic | 2.1s, four lines |
| Terms of access | documented, rate limit published | undocumented endpoint |
| Market and price data | none | its real strength |

#### The practical split

- **yfinance for prices, market data and quick recent snapshots.** It is genuinely the best tool for that and SEC does not compete.

- **yfinance for prototyping.** Four lines to a dataframe is worth a lot while you are still deciding what the question is.

- **SEC plus edgartools for anything published, anything historical, anything needing line items, and anything you might have to defend.**

- **A validation layer when the answer has to hold up.** That can be a full second pull, as it was here, or test-checks on a handful of key totals, which is often what time allows. Either is normal, and both sit alongside the oldest check there is, opening the filing yourself and reading the number.

## 13 What else edgartools does

Most of this write-up uses edgartools for one narrow job, financial statements, which undersells it considerably. It is an open-source project moving fast, and the surface is much wider than the part exercised here. A few things tested in the same session, each working first try:

| Feature | What happened when tested |
| --- | --- |
| **Full-text search across EDGAR** | `search_filings("artificial intelligence capital expenditures")` returned 20 results in 0.6 seconds. This searches filing *text*, which is exactly the gap XBRL leaves open. |
| **13F institutional holdings** | Berkshire's latest 13F-HR parsed into a dataframe of 90 holdings in 1.2 seconds, no manual XML handling. |
| **Insider transactions** | Nvidia's latest Form 4 parsed straight to the reporting owner and transaction detail in 1.5 seconds. |
| **Filing navigation** | Attachments, R-pages and taxonomy sidecars are addressable objects rather than URLs you assemble by hand. The download scripts linked here rely on this. |
| **TTM and period machinery** | Trailing-twelve-month calculation, quarterization, split adjustment, and multi-filing period stitching, all as first-class modules. |
| **Local storage and bulk download** | Caching and bulk local mirroring of EDGAR data, so repeat analysis does not re-hit the network. |
| **Notes to the accounts, as objects** | Amazon's latest 10-Q exposed 9 structured notes including Debt and Segment Information, each addressable by name and fuzzy-searchable. Not a text blob. |
| **Search inside a single filing** | `filing.search("capital expenditures artificial intelligence")` returned 18 BM25-ranked passages in 1.0 second, including the paragraph describing a $20.0 billion revolving credit facility. |
| **Dimensional and segment facts** | Amazon's 10-Q carries **391 dimensional facts**, which is the segment and business-unit breakdown underneath the headline numbers. An entire analytical layer most people never touch. |

Beyond that the package covers funds and NPORT portfolio holdings, N-MFP money market data, proxy statements, prospectuses, S-1 registrations, Schedule 13D/G, BDC entities, ownership forms 3/4/5 and 13F. Roughly 24 filing types parsed into typed objects, with about 178 public names at the top level. It is much closer to a full EDGAR client than a statements library.

## 14 The skills, and reading them properly

The most interesting thing found in the package is that **edgartools ships Claude skills inside itself**. Roughly 2,000 lines across seven skill directories, each with a `skill.yaml` of patterns and a `sharp-edges.yaml` of production gotchas. There is even a `constitution.yaml` defining weighted evaluation goals with anti-patterns, which is a skill-quality harness rather than documentation.

Installing them takes one command, and it worked first try:

```
pip install "edgartools[ai]"
python -c "from edgar.ai import install_skill; install_skill()"
# -> Installed to ~/.claude/skills/edgartools  (symlinked, so it updates with the package)
```

### Reading them changed three things in this write-up

This is worth being concrete about, because it is the strongest argument for the skills existing at all. Having done the whole analysis first and read the skills afterwards, three findings stood out:

| What the skill says | Effect |
| --- | --- |
| `concept-name-variance`, severity high | **It warns about the exact trap hit in section 07.** Concept names vary by company; use pattern matching or the normalising Financials API rather than an exact concept string. Reading this first would have saved discovering the Amazon and Nvidia capex difference the hard way. |
| `get-facts-instead-of-financials`, severity high | **A routing correction.** For standard metrics the idiomatic path is `get_financials()` with quick getters, not iterating raw facts. Tested: `get_revenue()`, `get_net_income()`, `get_total_assets()` returned in 0.7 seconds with no statement parsing at all. |
| `period-vs-instant` | **Independent confirmation** of the duration-versus-instant distinction in section 10, which was worked out the long way here. |

Also useful and previously unknown: `find()` is a universal entry point that resolves a ticker, a CIK, an accession number or a company name to the right object type, so you never construct a Filing by hand.

### Where that leaves the recommendation

**edgartools is the first port of call.** Not just for plain company facts, for most of this work. `Company("AAPL").get_financials().get_revenue()` against fourteen hand-built API calls is not a close contest, and the same holds for statements, filings, notes, holdings, insider forms and text search. It wins on effort and, more importantly, on the number of ways you can quietly get something wrong.

The raw SEC API stays in the toolkit for a short list of specific jobs. Cross-sectional screening through `frames`, which has no library equivalent and pulls thousands of filers in one call. Dependency-free one-off lookups at 20 KB. And second-path verification, where the point is precisely that it does not share the library's code or its assumptions.

So it is not a choice between them. It is a library for the work and a raw API for the breadth cases and the checking. Nothing in the skills changes the shape of that; they sharpen the routing inside it.

#### The principle worth keeping

**Pull from two independent paths whenever the number has to be right.** Everything here was reconciled across at least two. That is what turned the Tesla restatement in section 09 from an invisible three-million-dollar rounding difference into a discovered error in the dedup policy, one that had quietly broken a six-quarter series. A second source costs one script. The caveat from section 09 stands alongside it: make sure the paths are genuinely independent, because two implementations that share an assumption will agree perfectly and prove nothing.

## 15 Where to find it

edgartools is by **Dwight Gunning**, MIT licensed, free, no API keys and no subscription. At the time of writing it is past 2,000 stars with roughly 400 forks and over 3,700 commits, and it is maintained largely by one person while the SEC changes filing formats and XBRL taxonomies every year. That is a lot of unglamorous work absorbed on everyone else's behalf.

| Resource | Link |
| --- | --- |
| **Repository** | [github.com/dgunning/edgartools](https://github.com/dgunning/edgartools) |
| **Documentation** | [edgartools.readthedocs.io](https://edgartools.readthedocs.io/) |
| **AI integration overview** | [docs/ai/index.md](https://github.com/dgunning/edgartools/blob/main/docs/ai/index.md) |
| **The skills** | [docs/ai/skills.md](https://github.com/dgunning/edgartools/blob/main/docs/ai/skills.md). Install with `pip install "edgartools[ai]"` then `python -c "from edgar.ai import install_skill; install_skill()"`. Works with Claude Code and Claude Desktop. |
| **MCP server** | [docs/ai/mcp-setup.md](https://github.com/dgunning/edgartools/blob/main/docs/ai/mcp-setup.md). Run with `uvx --from "edgartools[ai]" edgartools-mcp` for Claude Desktop or any MCP client. |
| **Blog and examples** | [edgartools.io](https://www.edgartools.io) and the [notebooks](https://edgartools.readthedocs.io/en/latest/notebooks/) |
| **Issues and discussions** | [Issues](https://github.com/dgunning/edgartools/issues) and [Discussions](https://github.com/dgunning/edgartools/discussions) |
| **Sponsor** | [GitHub Sponsors](https://github.com/sponsors/dgunning) |

Given how fast it moves, read the repository and docs directly rather than trusting any write-up, including this one. The correction in section 09 exists precisely because the first pass here trusted the surface API instead of reading the source. If you use it seriously, star it, and if your work depends on it, sponsor it.

## 16 Points to watch

- **XBRL covers what is tagged.** Statements and notes are tagged; narrative discussion is not, in any structured way. If the reason capex jumped is explained only in prose, XBRL will not surface it. This limit is softer than it first appears, though: notes come back as addressable objects, and BM25 search inside a filing plus EDGAR-wide full-text search both reach the narrative. The gap is real but it is a gap in XBRL, not in what is reachable.

- **Earnings-call commentary is not in the filing at all.** Microsoft's capex-including-finance-leases figure exists only in spoken remarks, not in the 10-Q, the press release, or XBRL.

- **Restatements are quiet, and they are the sharpest edge here.** The same period appears repeatedly with different values across filings, and nothing flags it. Whoever writes the dedup rule decides which number is true. Take the latest filed value unless you are deliberately doing point-in-time work, never mix vintages inside one subtraction, and state the policy rather than defaulting into it. Section 09 shows what happens otherwise.

- **Frames silently under-covers,** and **yfinance silently drops old quarters.** Two different sources, same failure shape.

- **US filers only.** Foreign private issuers file 20-F on a different schedule with different tagging depth.

- **This is one session's testing on seven large-cap technology companies.** Smaller filers tag less consistently, and none of the coverage numbers here should be assumed to generalise without checking.

## 17 Index of scripts and files

Every number on this page came from one of these scripts. They open in the browser; use right-click and Save As to download. Each is standalone and needs only `pip install edgartools yfinance`, plus setting your own SEC identity where indicated.

| Script | What it does | Section |
| --- | --- | --- |
| [frames_coverage.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/frames_coverage.py) | Tests how many filers each quarterly frame actually contains, and which of the seven fall out | 03 |
| [fact_anatomy.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/fact_anatomy.py) | Anatomy of a single fact; duration versus instant across statements | 01, 10 |
| [download_filings.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/download_filings.py) | Saves the rendered filing, raw XBRL instance and all four taxonomy sidecars to disk | 04, 05 |
| [download_rpages.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/download_rpages.py) | Reads FilingSummary.xml and saves the SEC per-statement R-pages | 06 |
| [tag_standardization.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/tag_standardization.py) | Concept and custom-extension census across all seven cash flow statements | 07, 08 |
| [edgartools_pull.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/edgartools_pull.py) | Rebuilds the six-quarter grid via edgartools entity facts | 09 |
| [quarterize_test.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/quarterize_test.py) | Tests the library's own quarterize() against a hand-rolled de-cumulation | 09 |
| [restatement_audit.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/restatement_audit.py) | Earliest-filed versus latest-filed across all 84 cells, which found the Tesla error | 09 |
| [yfinance_pull.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/yfinance_pull.py) | The same grid via yfinance, with window and label diagnostics | 11 |
| [compare.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/compare.py) | SEC raw API versus edgartools, 84 cells | 09 |
| [compare3.py](https://www.tigzig.com/story/sec-xbrl-three-ways/code/compare3.py) | The three-way reconciliation, plus the period-label check | 11 |

#### Data outputs

| [rawapi_fcf.json](https://www.tigzig.com/story/sec-xbrl-three-ways/data/rawapi_fcf.json) | Derived quarters from the SEC company-concept API, latest-filed basis |
| --- | --- |
| [edgartools_fcf.json](https://www.tigzig.com/story/sec-xbrl-three-ways/data/edgartools_fcf.json) | The same grid via edgartools |
| [yfinance_fcf.json](https://www.tigzig.com/story/sec-xbrl-three-ways/data/yfinance_fcf.json) | The same grid via yfinance, with per-company window metadata |
| [tag_standardization.json](https://www.tigzig.com/story/sec-xbrl-three-ways/data/tag_standardization.json) | Full per-company concept census, including every custom extension and every financing and investing line item |
| [three_way_rows.json](https://www.tigzig.com/story/sec-xbrl-three-ways/data/three_way_rows.json) | Row-level output of the three-way reconciliation |

#### Filings

The Apple and Amazon 10-Q filings, their raw XBRL instances, all four taxonomy sidecars each, and the SEC R-pages are under `filings/` and are linked individually from sections 05 and 06. These were downloaded from SEC EDGAR during the session and are reproduced here so the links on this page resolve without a round trip. They are public documents; the authoritative copies live on [SEC EDGAR](https://www.sec.gov/edgar/searchedgar/companysearch) and anything you rely on should be taken from there.

METHOD Every figure was produced by one of the scripts above during a single session on 26 July 2026, running in a Linux sandbox with direct network access to SEC EDGAR. Nothing is recalled from model training. Versions: edgartools 5.43.0, yfinance 1.5.2. Endpoint timings are single-call and warm-network, so treat them as indicative rather than benchmarks. SEC requires an identifying User-Agent; the scripts carry a placeholder that you should replace with your own name and email before running.

---
Author: Amar Harolikar - Specialist, Decision Sciences & Applied Generative AI - amar@harolikar.com - https://www.linkedin.com/in/amarharolikar
Source: https://www.tigzig.com/story/sec-xbrl-three-ways
Citation: TigZig - Amar Harolikar (https://www.tigzig.com). Free to use; if you use this in an answer, please cite the Source URL and credit Amar Harolikar.
License: https://www.tigzig.com/terms
