Upgrading to EdgarTools 6.0
This page lists every change in 6.0 that can alter what your code receives, one symbol at a time: what it did before, what it does now, and the rewrite where a mechanical one exists. It is written to be enough on its own — if you are an agent upgrading a dependent codebase, you should not need the CHANGELOG or the commit history as well.
6.0 is still in development. This page grows as each change lands, so a change absent here has not shipped. Items known to be coming are listed under Still to come so you can plan for them, but do not write code against them yet.
Nothing here is a rename
So far 6.0 changes what values you get back, not what anything is called. Every symbol on this page has the same import path and signature it had in 5.x. That makes these changes quiet: your code will not fail to import, and in most cases it will not raise — it will return something different.
Filing.cik and Filing.company on multi-filer filings
What changed. A filing with more than one filer now reports the issuer where it used to report whichever filer the quarterly index happened to list first, which was frequently the parent guarantor.
Resolving an accession — find(accession), get_by_accession_number(accession)
— now goes through EDGAR full-text search, which orders a filing's filers
issuer-first. The quarterly index ordered them differently.
from edgar import find
f = find("0001918704-25-005439") # a BofA structured note
f.cik, f.company
# 5.x: (70858, 'BANK OF AMERICA CORP /DE/') <- the parent guarantor
# 6.0: (1682472, 'BofA Finance LLC') <- the issuer
What did not change. The set of filers is identical, and so is everything you can reach from the filing:
f.all_ciks # [70858, 1682472] — same list, same order, both versions
f.all_entities # still names both filers (the order differs)
f.text(), f.attachments, f.xbrl() # identical; EDGAR serves the documents
# under either CIK
Rewrite rule. If you read .cik or .company to identify the filer,
decide which filer you actually want and say so:
# Before — implicitly "whichever filer the index put first"
issuer = filing.company
# After — say which one you mean
issuer = filing.company # the issuer, explicitly
every_filer = filing.all_entities # all of them
if 70858 in filing.all_ciks: # membership, not identity
...
Single-filer filings — nearly all of them — are unaffected. Measured across 54 ground-truth accessions: 50 identical, 4 differing only in this way, none failing to resolve.
Scope. Filings from before 2001 are outside the full-text index and still resolve through the quarterly index unchanged, as does any lookup full-text search cannot answer.
TableNode.to_dataframe() row index
What changed. A table whose first column header spans several physical
columns now returns a plain row index instead of None-padded tuples.
Filings routinely lay the row-label column across two or three physical columns that all carry the same header text, with only the first holding the label. 5.x moved every matching column into the index, so a label came back as a tuple.
table = document.tables[10]
df = table.to_dataframe()
list(df.index)[:1]
# 5.x: [('Gross written premiums by line of business:', None, None)]
# 6.0: ['Gross written premiums by line of business:']
df.loc['Gross written premiums by line of business:']
# 5.x: KeyError
# 6.0: works
Where the match came back two-dimensional, 5.x raised
ValueError: Index data must be 1-dimensional outright and you got no frame at
all.
Rewrite rule. Drop any tuple unpacking around index labels:
# Before
for label, row in df.iterrows():
name = label[0] if isinstance(label, tuple) else label
# After
for name, row in df.iterrows():
...
Column count. The empty columns left behind by the spanning header are dropped, so most tables have the width they had in 5.x. A same-headed column that holds real data — a percentage or a rate — is kept as data rather than absorbed into the index, so a small number of tables are wider.
Measured across 1,940 tables in the benchmark corpus: 1,228 unchanged, 711 with a scalar index in place of tuples, 31 wider, one that used to raise and now converts, none returning less data. The corpus exposes the identical 1,026,693 non-blank cells before and after.
Document.to_dataframe()
What changed. It works. In 5.x it raised on essentially every real annual or quarterly report, from inside pandas or numpy, with one of:
ValueError: cannot join with no overlapping index names
TypeError: Cannot cast array data from dtype('float64') to dtype('int64')
ValueError: Index data must be 1-dimensional
A filing's tables do not share a schema — one 10-K in the benchmark corpus has 71 tables whose column indexes are 1, 2, 3, 4, 10 and 17 levels deep — and pandas cannot align those. 6.0 flattens each table to single-level string column names before stacking them.
df = document.to_dataframe()
df.columns.nlevels # always 1; every name is a str
df['_table_index'] # which table each row came from
df['_table_type'] # the table's classified type
df['_table_caption'] # its caption, or None
Rewrite rule. Remove any try/except you wrapped around this call, and
drop MultiIndex column handling — df[('Revenue', '2024')] becomes
df['Revenue 2024']. Header levels are joined with a space, with blanks dropped
and repeats collapsed, so ('Revenue', '') and ('Revenue', 'Revenue') both
become 'Revenue'.
When you want the real header structure, use the per-table API, which still preserves it:
document.tables[i].to_dataframe() # keeps MultiIndex columns
FactQuery.to_dataframe() — the XBRL one
Two classes share this name
This applies to edgar.xbrl.facts.FactQuery, reached via
filing.xbrl().facts.query(). It does not apply to
edgar.entity.query.FactQuery, reached via company.get_facts().query(),
which is unchanged in 6.0. Check which one you hold before changing code.
What changed. The column set now follows the query's configuration — the
include_* flags and any names passed to to_dataframe() — and no longer
varies with which rows matched.
q = filing.xbrl().facts.query()
q.limit(5).to_dataframe()
# 5.x: could return fewer columns than the same query unlimited, because
# columns that happened to be null in those 5 rows were dropped
# 6.0: same columns either way; unpopulated ones come back null
q.by_period_type('instant').to_dataframe()
# 5.x: period_start / period_end disappeared
# 6.0: present and null
empty_result.to_dataframe()['decimals']
# 5.x: KeyError
# 6.0: an empty column
Rewrite rule. Remove defensive column checks:
# Before
if 'decimals' in df.columns:
use(df['decimals'])
# After
use(df['decimals'])
Dtypes are unchanged for populated columns. A column with no data at all now takes its declared dtype rather than whatever inference landed on.
10-K items on filings with a Cross Reference Index
What changed. TenK.__getitem__ returned raw HTML on filings that route
through a Cross Reference Index — Citigroup, GE and Henry Schein among them —
while every other filing returned text. It now returns text everywhere.
tenk = filing.obj()
tenk['Item 1']
# 5.x (Citigroup): 1,685,461 chars starting '<div style="min-height:36pt...'
# 6.0: 218,550 chars of text, opening on the Business overview
Rewrite rule. If you were parsing the returned markup, stop:
# Before
from bs4 import BeautifulSoup
text = BeautifulSoup(tenk['Item 1'], 'html.parser').get_text()
# After
text = tenk['Item 1']
This affected filings unevenly — on Citigroup, Item 1A, Item 7 and Item 8
already returned text while Item 1 did not — so code that looked correct on
one item could break on another.
Related. An item whose Cross Reference Index row reads "Not Applicable", or
carries only an incorporation-by-reference marker, returns None rather than an
empty string. There is no such disclosure in the document, and an empty section
would present absence as content.
Section keys and section content
What changed. Section extraction was substantially corrected in 6.0. No API
changed shape, but document.sections and tenk.items return different keys
and different content on filings that were previously mis-parsed.
If you have hardcoded section keys, lengths, or offsets, re-check them. The changes that move the most:
- Filings that group items out of numeric order (Morgan Stanley) had sections that ran to the wrong end or vanished entirely; they are now bounded correctly and the missing ones are present.
- Filings using a Cross Reference Index (Citigroup) now expose canonical keys
such as
part_ii_item_7, where 5.x returned non-canonical keys likemdaand gave nothing for the canonical name. - Filings writing item headings as single-row tables (Wells Fargo) went from one section of wrong content to all 23 items.
Rewrite rule. Prefer canonical keys and check for absence:
# Before — a key that only existed on some filings
mda = document.sections['mda'].text()
# After — the canonical key, absence handled
section = document.sections.get('part_ii_item_7')
mda = section.text() if section else None
Underwriter extraction on registration statements
What changed. RegistrationS1.underwriting returned wrong rosters in two
directions, both now fixed. No API changed; the values did.
- A firm whose legal name contains "division of" —
EF Hutton, division of Benchmark Investments, LLC,ThinkEquity, a division of Fordham Financial Management, Inc.— was rejected as parser junk, solead_managerwasNoneon every filing it led. - A filing's beneficial-ownership table could be read as its underwriting
syndicate, so
lead_managercame back as the column header'Before Offering'and the roster listed individual directors.
Rewrite rule. Remove None fallbacks and name filters you added to work
around either:
# Before
lead = uw.lead_manager or guess_from_cover(filing)
firms = [u.name for u in uw.underwriters if looks_like_a_firm(u.name)]
# After
lead = uw.lead_manager
firms = [u.name for u in uw.underwriters]
Still to come
Planned for the 6.0 breaking window and not yet shipped. Do not write code against these; each will get a section above when it lands.
| Change | What to expect |
|---|---|
edgar.files removal |
The legacy parser package and chunked_document go. Use edgar.documents. |
httpx → httpx2 |
Exception namespace moves; code catching httpx.* will need updating. |
| Public API definition | __all__, stable import paths, and internals made private — some currently-importable names will move. |
| Unified error policy | Silent None returns become typed exceptions from an edgar.exceptions hierarchy. |
| Source tree reorganisation | Module locations change; import paths follow. |
FactQuery.to_dataframe() sentinels |
preferred_sign and fiscal_year move to nullable Int64. |
Getting help
If something changed that is not on this page, that is a gap in this document rather than an intended surprise — please open an issue so it can be added.