Twenty-seven entries, led by the XBRL dimensional and calculation models. Both were keyed too coarsely — axes and domains by concept when they belong to a role, calculation edges by concept when a concept can roll up into two totals — so each returned one answer where the filing gives several.
Upgrading. Several of these change values you already read, all in the direction of returning more or refusing to guess:
xbrl.axesandxbrl.domainskeep their shape but are now a union across roles rather than whichever role was parsed last. Use the newxbrl.axes_for_role(role_uri)/xbrl.domains_for_role(role_uri)wherever the role is known — the flat views cannot distinguish a domain's two-way breakdown on the income statement from its five-way one in a revenue note.calculation_linkbase()returns more rows: one per filed relationship rather than one per concept. Coca-Cola's FY2023 10-K goes from 278 rows to its filed 306. Code that assumed one row per concept will now see a concept twice, with a different parent and weight — which is what the filing says.get_revenue(unit=...)andget_gross_profit(unit=...)returnNonefor a unit the company does not report in, where they previously returned a USD figure. If you passedunit='EUR'and got a number, that number was dollars.- Footnote text changes for footnotes containing markup: text outside a
<div>is no longer dropped and nested text is no longer duplicated.related_fact_idsnow hold fact ids rather than locator labels, soget_footnotes_for_fact()resolves on filings where it previously returned nothing. Report.get_dataframe()returns data instead of an empty DataFrame, andDocument.chunks()no longer raisesModuleNotFoundError. Both were broken for every filing.CurrentReport.docnow warns; it was the last un-warned public route intoedgar.files, which 6.0 deletes. Use.document, or.items/report[item].
pip install --upgrade edgartoolsAdded
- Role-scoped access to the XBRL dimensional model.
XBRL.axes_for_role(role_uri)andXBRL.domains_for_role(role_uri)return the axes and domains a single extended link role declares, keyed on element ID. Prefer them overxbrl.axes/xbrl.domainswherever the role is known: the same domain routinely carries different members in different roles, so the flat views can only offer the union.AxisandDomaingainrole_uri, andXBRL.axes_by_role/XBRL.domains_by_roleexpose the underlying store. CalculationTree.all_arcs, one entry per filed calculation relationship. A concept may roll up into two totals with a different weight, and often a different sign, under each, which the one-node-per-conceptall_nodesmap cannot represent.all_nodesis unchanged and still the right thing for lookup and membership;all_arcsis the graph.ElementCatalog.typed_domain_refandElementCatalog.substitution_group, read from the element declaration, which is what letsAxis.is_typed_dimensionreport a dimension the filer declared as typed.
Fixed
-
get_revenue(unit=...)answered a request for one unit with a figure in another. When the unit filter correctly rejected every candidate fact,_get_standardized_concept_valuefell through to its component calculation, and that calculation checked the two components against each other — which establishes only that they can be added — before normalising with no target unit at all. Apple'sGrossProfitandCostOfGoodsAndServicesSoldare both USD, so their sum was returned as the answer tounit='EUR'and tounit='shares'alike: 364,357,000,000 in both cases, and both nowNone. The default USD path is unchanged and still answers from the revenue fact itself. Strictness now reaches the fallbacks, and a derived figure has to answer the unit that was asked for — an exact match always does, a merely compatible one only where the caller did not pin the unit, which is the rule the direct path already used.get_gross_profit()was leaking the same way for a second reason — its own direct branch never asked for strict matching at all, so a compatible-but-different currency succeeded before the guard was even reached — and both fallbacks now answer by the same rule as the direct path. This was latent until bare concept names were indexed (GH #1202, same release): before that,get_fact('GrossProfit')returnedNone, the components were never found, and the fallback returnedNonefor the wrong reason. (bead edgartools-885h) -
Report.get_dataframe()returned an empty DataFrame for every filing.FinancialTableExtractorreadtable_node._processed, an attribute that exists only on the legacyTableNode, while the module parses with the modern one — so every call raisedAttributeErrorinto a bareexceptthat returned an empty frame. Measured before the fix: 642 of 642 tables across 21 filings raised, and 0 of 42 Apple R-files produced data; after, 41 of 42, with Apple's Q2 FY2025 net sales reading the filed $95,359M and $219,659M. Two further defects surfaced once the code could run: a short period-header row was padded on the left, attributing every figure to the wrong period, andAUDwithout a word boundary matched inside "Unaudited", so an unaudited USD statement reported Australian dollars. The money scale now prefers the currency-qualified phrase, since "shares in Thousands, $ in Millions" was read as thousands. (bead edgartools-yq1l) -
Document.chunks()raisedModuleNotFoundErroron every call. It importededgar.documents.extractors.chunk_extractor, a module that was never written, so this public retrieval API — token-budgeted chunks with overlap, the shape a RAG pipeline asks for — has been unusable since the parser rewrite, along with theDocumentChunkclass that was only reachable through it. The module now exists;overlap >= chunk_sizeraisesValueErrorrather than advancing the window by zero and looping forever. (bead edgartools-vwtb) -
Two taxonomies whose namespace URIs end in the same path segment became one concept. A concept is identified by its expanded name — namespace URI plus local name — and the prefix is only a display choice, but when a namespace was declared on the fact element rather than the instance root the parser fell back to deriving the prefix from the URI's final path segment.
{http://example.com/l01/alpha/2024}Collisionand{http://example.com/l01/beta/2024}Collisionboth became2024:Collision: both facts survived, their technical identity did not, and grouping by concept silently merged concepts from different taxonomies. The prefix a document declares is now read wherever it declares it, and a derived segment already claimed by a different namespace is suffixed rather than shared. Real filings declare their namespaces on the instance root, so nothing changes for them — Apple FY2023, Microsoft FY2024 and Coca-Cola FY2023 return the same 1,640 concept strings over 6,138 facts as before, byte for byte. Separately,Axis.is_typed_dimensionandAxis.typed_domain_refwere declared on the model and written by no code path at all, so every dimension read as explicit;xbrldt:typedDomainRefis now carried from the element declaration through the element catalog onto the axis. An axis declared only in an imported taxonomy still reads as explicit, because followingxs:importinto a remote taxonomy is a separate capability that remains open. (GH #1188, #1234, bead edgartools-0c1q.16) -
Footnote-to-fact links resolved to nothing, and footnote text was truncated and duplicated at once. A
footnoteArc'sxlink:fromnames alink:locwhosehreffragment is the fact id, and edgartools stored the locator label instead — so Coca-Cola's FY2011 10-K put six ids on its footnote that name no fact, no fact carried a footnote back, andget_footnotes_for_fact()returned nothing for a fact that has one. This resolved almost everywhere by coincidence, because 18,686 of the 18,699 footnote locators in the fixture corpus label a locator with the fact id verbatim; the 13 that do not are in Coca-Cola FY2011 and JPMorgan FY2012, and both are now fully linked. Footnote text was built from descendant<div>s whenever any existed, so direct-child and sibling text was dropped, and sincefindall('.//div')returns nested divs alongside their ancestors whileitertext()already descends, nested text was emitted twice; a footnote in Golub Capital's Q1 FY2025 10-Q lost its opening sentence and its entire table while repeating one sentence, reading 187 words against 352. Text is now assembled in one ordered walk with a break after each block, so words no longer glue across boundaries. Footnote resources are also matched within their ownfootnoteLinkrather than a document-wide dict, since two extended links may legitimately reuse a label — a collision returned a real footnote belonging to a different fact. Of the 123 footnotes across the three fixtures that exercise this, only the 3 with nested markup change; the rest are byte-identical. (GH #1169, #1190, #1230, beads edgartools-0c1q.15, edgartools-05gk) -
calculation_linkbase()dropped a concept's second calculation parent, and the weight that came with it. Calculation relationships were stored one node per concept, so a concept rolling up into two totals in the same role kept only whichever edge the traversal reached last — andcalculation_linkbase()documents its result as one row per parent-to-child relationship. Multiple parents are ordinary rather than exotic: Coca-Cola's FY2023 10-K returned 278 rows against 306 filed arcs, Apple's 194 against 215 and JPMorgan's 438 against 456, and each returns exactly its filed count now. The sign was the sharper half — Coca-Cola filesOtherComprehensiveIncomeAvailableforsaleSecuritiesAdjustmentNetOfTaxPortionAttributableToNoncontrollingInterestinto one total at+1and another at-1, so the single row that survived reported a weight whose sign depended on traversal order.CalculationTreenow carriesall_arcs, one entry per filed edge, beside theall_nodesmap that callers use for lookup and membership. The same error at the tree level is fixed with it: a role split across twocalculationLinkelements, which is legal XBRL, had its second element replace the whole tree built from the first, so relationships are now accumulated across every element and file before the trees are built. (GH #1184, #1238, bead edgartools-0c1q.14) -
The dimensional model was not role-scoped, so a domain reused across roles kept only one role's members. Hypercubes were stored per extended link role, but axes and domains sat in dicts keyed on element ID alone with no role on either model, and the members of a domain were assigned unconditionally once per role. Apple's FY2023 10-K splits
srt:ProductsAndServicesDomaintwo ways on the income statement and five ways in the revenue note; a single global object held whichever was parsed last, so one of those two roles was always answered with the other's breakdown. Axes and domains are now kept per role, reachable throughxbrl.axes_for_role()andxbrl.domains_for_role(), and the existingxbrl.axes/xbrl.domainskeep their shape as a union across roles rather than a truncation. Five further gaps in the same loop close with it:xbrldt:targetRoleis followed across extended links, so a hypercube whose dimensions are declared in another role is no longer reported as having none;dimension-defaultarcs setAxis.default_member_id, without which a fact at the axis default cannot be told from an undimensioned one;Table.closedandTable.context_elementare read from theallarc instead of being hardcoded;Domain.parentis written for a member that is itself a parent; and axes and members follow the filedorderattribute, which was already being extracted and then discarded. Measured on Exxon's FY2022 10-K: 49 of 49 hypercubes now report theclosed="true"they were filed with against 0 before, all 39 axes carry their default member against 0, and 37 domains carry a parent against 0. (GH #1171, #1194, #1219, #1225, #1235, #1236, bead edgartools-0c1q.13) -
get_statement_facts()returned nothing for concepts that are demonstrably in the statement. A fact's statement membership was read from whichever presentation role the enrichment loop reached first and stored as a single scalar, but membership is a set —us-gaap:NetIncomeLossis presented in the income statement, the cash flow statement, the statement of equity and more. Apple's FY2024 10-K returned 0 rows forNetIncomeLossinCashFlowStatement,ComprehensiveIncome,StatementOfEquity,NotesandDisclosures, and 6 in each after the fix; statement row counts rise correspondingly, for instanceStatementOfEquityfrom 25 to 63. The full membership is now accumulated once into an index — strictly less work than the per-fact tree scan it replaces — andby_statement_type()tests membership rather than equality. The scalarstatement_typekeeps its meaning as the primary statement, and the membership lists stay off the declared DataFrame schema. (GH #1242) -
get_facts_with_dimensions()removed the dimension columns it selected rows on. The predicate matches a fact because it carriesdim_*keys, thento_dataframe()'s projection dropped every one of them becauseinclude_dimensionswas left falsy, so the caller got the right rows with the information that made them the right rows deleted. Intel's FY2024 10-K returned 1,135 correctly-selected rows and 0 dimension columns; it now returns the same rows with all 56. (GH #1243) -
pivot_by_dimension()silently returned a table that was not a pivot. The lookup column was built by interpolating the caller's spelling, while the projected column is always normalised, so the natural QName formus-gaap:AwardTypeAxismissed and the method fell through to returning the plain unpivoted frame — a DataFrame of plausible shape that a caller who does not already know the pivoted layout cannot distinguish. All three spellings (dei_LegalEntityAxis,dei:LegalEntityAxis,LegalEntityAxis) now resolve to the same pivot, and an axis no fact carries raisesValidationErrornaming the available axes instead of returning an empty frame that reads as "no data". Both pivot methods also now keep colliding rows apart by unit where that is what distinguishes them, and report any collision they cannot resolve rather than quietly keeping one row of each. (GH #1223) -
Unitless facts were classified as numeric, so numeric queries returned document metadata.
float(value)ran on every fact unconditionally, sodei:DocumentFiscalYearFocus— agYearwith no unit — carried anumeric_valueof 2024.0 and a query for facts valued between 2020 and 2030 returned it alongside real monetary facts. XBRL requires aunitRefon numeric items and forbids one on non-numeric items, which makes the unit an exact test where the element catalog is not reachable. Five to seven facts per filing are reclassified — CIK, fiscal-year focus, area code, auditor firm ID, postal code — with every genuine numeric fact untouched.numeric_valueis what consumers read as "this is a numeric fact", so this reached well beyondby_value(). (GH #1220) -
Inline XBRL facts from
parse_html()carried no resolved context or unit._extract_contextsand_extract_unitsran namespace-aware XPath (//xbrli:context) against a tree built bylxml.html.fromstring(), whose elements carry the literal tag"xbrli:context"and no namespace URI, so the lookups matched nothing and every fact's.contextand.unitcame backNonewhile the raw refs sat on the fact untouched. Matching on the local name instead resolves 180,747 of 180,747 context references and 169,255 of 169,255 unit references across the 60 iXBRL filings in the fixture corpus, where the previous count was zero of each. A<unit>built fromxbrli:dividenow resolves toUSD/sharesrather than to plainUSD, and dimensional members are read fromxbrli:scenarioas well asxbrli:segment. (GH #1232) -
A fact's value was the text before its first child, and nothing else.
_get_fact_valuereadelement.text, so descendant markup, tail text and the entirecontinuedAtchain were dropped and the truncated string looked like a complete value — the mechanism behind four separate reports. Relevant content is now assembled properly: descendants and their tails contribute, anix:excludesubtree does not while the text following it does,escape="true"serializes child markup instead of flattening it, and continuation chains are followed in order with a cycle guard.ix:footnoteandix:continuationare resources rather than facts and are no longer routed through the fact extractor, which removes 451 blank fake facts from the same corpus. SincecontinuedAtis how issuers split long narrative disclosures, the truncation fell on exactly the text an LLM reads. (GH #1189, #1237, #1239) -
Unsupported inline transformations passed human display text through as the fact value. The registry held five entries and matched them on an exact QName, so it missed even
ixt:num-dot-decimal— the TR4 spelling that accounts for 68,880 of the 90,000formatattributes in the fixture corpus — and returned"Delaware"where the value is"DE"and"ten years"where it is"P10Y". It now implements Transformation Registry 1–4 and the SEC'sixt-secextensions keyed on a version-independent name, covering every format those 60 filings use, and a format that cannot be applied is recorded on the fact and logged rather than silently yielding display text. Scale is computed withDecimal: applying it in binary float wrote the error into the lexical value, so a filed 0.7 atscale="-2"became0.006999999999999999and one filing reported8199999999.999999for $8.2 billion. (GH #1250, #1251) -
Document.has_xbrlreportedFalseon documents with thousands of extracted facts. The inline pre-process pass stores its facts onmetadata.xbrl_data, whileDocument.xbrl_factscalled a separate extractor that scanned the node tree forix_tagmetadata the inline pass never sets — two unconnected pipelines, so the public surface answered from one that was never populated. This is worse than missing data, because a working extraction looked like an absent feature and a caller correctly branched away from it. The property now reads the pass that is populated, andhas_xbrlisTruefor all 60 iXBRL filings in the fixture corpus against zero before. (GH #1233) -
Every XBRL duration was reported one day short. XBRL 2.1 reads a date-only
endDateas the end of that day, so the context runs to 24:00 on it and the final day belongs to the period;reporting_periods[*]['days']subtracted the two dates and dropped it. A calendar year of 2023-01-01 to 2023-12-31 measured 364, Apple's 53-week FY2023 measured 370 instead of 371, and a duration context covering a single day measured 0. The count now lives in one place,edgar.xbrl.core.duration_days, used for the stored field and for the two sites inperiod_selectorthat recompute a duration and compare it against that field.periods.pykeeps its own exclusive count, because its bucket bounds were calibrated against it and a 53-week fiscal year sits exactly on the<= 370bound - it no longer writes that local value back over the public field, which is what made the field mean different things depending on whether a statement had been rendered first. Measured over the 8 filings underdata/xbrl/datafiles: 191 duration periods change theirdaysvalue and nothing else changes - noperiod_type,fiscal_period,labelorkey, and no change to the periods selected for any statement. (GH #1247) -
Table rows were dropped at random.
<thead>rows were deduplicated byid(), but nothing kept the lxml element proxies alive — lxml frees a proxy once unreferenced and CPython reuses the address, so a tbody row could inherit a stale thead id, match it, and be skipped as "already processed". The row vanished from the table with no error. Measured on the ABRAMS Form 40000001923-04-000001: 1 to 22 divergences per 25 renders before, 0 in 100 after. Membership is now tested by identity against a list held for the whole function, so the collision is impossible by construction rather than dependent on GC timing. This is what made the regression lane fail at random. (bead edgartools-gf6v) -
EntityFactslookups reported that present data did not exist. The fact index was keyed by the qualified name a fact is tagged with (us-gaap:StockholdersEquity) and by the lowercased label, never by the bare concept name — soget_annual_fact('StockholdersEquity')returnedNoneand warned "No fact found", while the value was present and correct.get_annual_fact('Assets')worked only becauseus-gaap:Assetshappens to be labelled "Assets" and matched the label key by coincidence, which is what made this look intermittent rather than systematic: in Snowflake's company facts that coincidence holds for 4 of 339 concepts. The local name is now indexed too, in its filed case — a lowercased form would merge with existing label keys. Across AAPL, JPM and KO, 1,954 of 4,264 concept and label lookups that previously answeredNonenow return a fact, with no existing answer changed or lost.get_fact()andavailable_periods()shared the defect and share the fix. (GH #1202) -
A filtered or paged
Reportscollection returned reports whose content could not be read.Report.contentfetches its R-file throughreports._filing_summary._filing_sgml, and three of the four methods that return a new collection rebuilt it without that back-reference, sofilter(),next()andprevious()handed back reports with the right filenames and raisedAttributeError: 'NoneType' object has no attribute '_filing_sgml'on.content. On Apple's FY2024 10-K a multi-row filter now reads R3.htm as the 79,891 charactersget_by_category()always returned. Only the multi-row branch offilter()was affected — filtering to a single report returns aReportstill attached to the original collection, which is why this stayed hidden. Deriving a collection now goes through one place, so the next method to return a subset carries the context by default. (GH #1191) -
FactQuery.scale()was a silent no-op on every parsed fact. Transforms were applied tofact['value'], which holds the filed string, soscale()'s ownisinstance(value, (int, float, Decimal))guard rejected it and handed it straight back — whilenumeric_value, the float every consumer actually reads, was never transformed on any path.scale(1000), the example in its own docstring, changed nothing. A numeric fact is now transformed on its numeric value and both fields are written together so they cannot disagree; a non-numeric fact keeps being transformed onvalue, so text transforms still work andscale()leaves TextBlocks alone through the same guard.StitchedFactQueryduplicated the defect and shares the fix. (GH #1187) -
FactQuery.to_dataframe()served a stale table after the query was narrowed. The cache was keyed on the column projection alone, so the firstto_dataframe()answered every later one: after.by_concept('us-gaap:Assets'),execute()returned 2 rows whileto_dataframe()still returned the original 1,075, with nothing to say the two disagreed.FactQueryis a fluent mutable builder whose methods returnself, so one object describes a different population after each call, and the key now covers the query configuration as well as the projection. Identical queries are still served from the cache.StitchedFactQueryduplicated this one too. (GH #1186) -
A disclosure whose subject is notes payable was routed to
notes().get_all_statements()falls back to keyword matching when a role carries noFilingSummarymenu category, and that fallback tested the bare substring"note"before it tested"disclosure". "Note" names a financial-statement section and a debt instrument, so Oracle's fourDisclosure - NOTES PAYABLE AND OTHER BORROWINGSroles (10-Q0000950170-23-047713) reportedtype="Notes"/category="note", came back fromxbrl.notes(), and were absent fromxbrl.disclosures(). The ambiguous word no longer outranks a role that states what it is - by itsDisclosurecategory marker, or by the concept it hangs from (us-gaap_DebtDisclosureAbstract) - unless the definition names the notes section itself, soNotes to Consolidated Financial StatementsandNote 1 - Organizationstill classify as notes. Roles whose definition does not contain "note" are classified exactly as before. (GH #1207)
Changed
Filing.sections()is chunked byedgar.documentsinstead of the legacy parser. It backsFiling.search(), and its old backend is deleted in 6.0, so it needed a replacement rather than a deprecation. Chunks are cut at headings with each table its own chunk, capped so no chunk swallows a whole Item. Measured across 41 era-stratified fixtures the two chunkers index the same words to within 0.10%, and across 375 phrase queries mean recall@5 is unchanged at 99.2%. (bead edgartools-07lk.3)
Deprecated
CurrentReport.docwarns; it was the last un-warned public route intoedgar.files. It returns the legacyChunkedDocument, whileCompanyReport.docon every other report class returns the modernedgar.documentsdocument — so the same attribute name yields two unrelated types depending on the form, and a survey starting at the base class reads.docas already migrated. Use.documentfor the parsed document, or.items/report[item]for item access. Measured across 133 era-stratified fixtures and 2,128 item lookups, no internal code path reaches the legacy parser through the report classes any more. (bead edgartools-07lk.3)