diff --git a/changelog.d/u6tkw-schedule13-unreported.changed.md b/changelog.d/u6tkw-schedule13-unreported.changed.md
new file mode 100644
index 000000000..497539cee
--- /dev/null
+++ b/changelog.d/u6tkw-schedule13-unreported.changed.md
@@ -0,0 +1 @@
+**Schedule 13D/G figures a filing does not report are `None`, not `0`.** `ReportingPerson`'s ownership figures and power totals, `total_shares`/`total_percent`, and `OwnershipComparison.shares_change`, `percent_change` and `get_summary()` no longer read a missing figure as 0, so a 13D/A omitting the Aadi Bioscience share counts is no longer a 2,435,000-share sale; a reported 0 stays 0, and the 5.x FutureWarning is gone. (bead edgartools-u6tkw)
diff --git a/docs/guides/schedule13dg-data-object-guide.md b/docs/guides/schedule13dg-data-object-guide.md
index d052cdc6c..03d7dfa3c 100644
--- a/docs/guides/schedule13dg-data-object-guide.md
+++ b/docs/guides/schedule13dg-data-object-guide.md
@@ -72,8 +72,8 @@ When you call `filing.obj()` on a Schedule 13D filing, edgartools parses the XML
| `amendment_number` | `Optional[int]` | Amendment sequence number |
| `is_amendment` | `bool` | Whether this is an amendment (`/A`) |
| `filing_date` | `date` | Date filed with the SEC |
-| `total_shares` | `int` | Aggregate beneficial ownership (handles joint/separate filers) |
-| `total_percent` | `float` | Aggregate ownership percentage |
+| `total_shares` | `Optional[int]` | Aggregate beneficial ownership (handles joint/separate filers) |
+| `total_percent` | `Optional[float]` | Aggregate ownership percentage |
### Items (Narrative Disclosures)
@@ -151,8 +151,8 @@ When you call `filing.obj()` on a Schedule 13G filing, edgartools parses the XML
| `rule_designation` | `Optional[str]` | SEC rule under which filing is made (e.g., "Rule 13d-1(c)") |
| `is_amendment` | `bool` | Whether this is an amendment |
| `filing_date` | `date` | Date filed with the SEC |
-| `total_shares` | `int` | Aggregate beneficial ownership |
-| `total_percent` | `float` | Aggregate ownership percentage |
+| `total_shares` | `Optional[int]` | Aggregate beneficial ownership |
+| `total_percent` | `Optional[float]` | Aggregate ownership percentage |
| `is_passive_investor` | `bool` | Always `True` for Schedule 13G |
### Example: Reading a Schedule 13G
@@ -240,20 +240,20 @@ Each reporting person (individual or entity) in the filing.
| `cik` | `str` | SEC Central Index Key |
| `name` | `str` | Person or entity name |
| `citizenship` | `str` | Citizenship or place of organization |
-| `sole_voting_power` | `int` | Shares with sole voting authority |
-| `shared_voting_power` | `int` | Shares with shared voting authority |
-| `sole_dispositive_power` | `int` | Shares with sole dispositive authority |
-| `shared_dispositive_power` | `int` | Shares with shared dispositive authority |
-| `aggregate_amount` | `int` | Total shares beneficially owned |
-| `percent_of_class` | `float` | Percentage of outstanding shares |
+| `sole_voting_power` | `Optional[int]` | Shares with sole voting authority |
+| `shared_voting_power` | `Optional[int]` | Shares with shared voting authority |
+| `sole_dispositive_power` | `Optional[int]` | Shares with sole dispositive authority |
+| `shared_dispositive_power` | `Optional[int]` | Shares with shared dispositive authority |
+| `aggregate_amount` | `Optional[int]` | Total shares beneficially owned |
+| `percent_of_class` | `Optional[float]` | Percentage of outstanding shares |
| `type_of_reporting_person` | `str` | Entity type code (e.g., "IN" = individual, "IA" = investment adviser) |
| `member_of_group` | `Optional[str]` | `"a"` = joint filer, `"b"` = separate filer |
-| `total_voting_power` | `int` | Computed: sole + shared voting power |
-| `total_dispositive_power` | `int` | Computed: sole + shared dispositive power |
+| `total_voting_power` | `Optional[int]` | Computed: sole + shared voting power |
+| `total_dispositive_power` | `Optional[int]` | Computed: sole + shared dispositive power |
| `unreported_fields` | `frozenset[str]` | Names of the six ownership figures above that the filing does not report |
| `reported(figure)` | `Optional[int \| float]` | An ownership figure by field name, or `None` when the filing does not report it |
-An amendment (13D/A, 13G/A) may leave any of the six ownership figures out, and a pre-2025 filing read from its SGML header has identities only. Such a figure holds a placeholder `0` in its field and is named in `unreported_fields`; `reported()` returns `None` for it, and `0` for a reported zero. In edgartools 6.0 the fields themselves become `None` for an unreported figure, so code that uses `reported()` today keeps working unchanged.
+An amendment (13D/A, 13G/A) may leave any of the six ownership figures out, and a pre-2025 filing read from its SGML header has identities only. Such a figure is `None` and is named in `unreported_fields`, and `total_voting_power` or `total_dispositive_power` is `None` when either half is. A reported zero stays `0`. `reported(figure)` returns the same value as the field, looked up by name.
### IssuerInfo
@@ -320,24 +320,22 @@ amendment = Schedule13D.from_filing(amended_filing)
comparison = OwnershipComparison(current=amendment, previous=original)
-change = comparison.reported_shares_change
+change = comparison.shares_change
if change is None:
print("Shares changed: unknown, a filing does not report its share count")
else:
print(f"Shares changed: {change:+,}")
-if comparison.reported_percent_change is not None:
- print(f"Percent changed: {comparison.reported_percent_change:+.1f} points")
+if comparison.percent_change is not None:
+ print(f"Percent changed: {comparison.percent_change:+.1f} points")
print(f"Accumulating: {comparison.is_accumulating}")
print(f"Liquidating: {comparison.is_liquidating}")
```
-`reported_shares_change` is `None` when either filing does not report its share counts: an amendment that leaves them out (the schema makes them optional on 13D/A and 13G/A), a pre-2025 header-only filing (`has_structured_data` is `False`), or a filing with no reporting-person rows. `is_accumulating`, `is_liquidating` and `is_unchanged` are then all `False`. `reported_percent_change` is `None` in the same way for percentages, and the two are checked separately, since a filing can report one and not the other. A reported zero stays `0`.
+`shares_change` is `None` when either filing does not report its share counts: an amendment that leaves them out (the schema makes them optional on 13D/A and 13G/A), a pre-2025 header-only filing (`has_structured_data` is `False`), or a filing with no reporting-person rows. `is_accumulating`, `is_liquidating` and `is_unchanged` are then all `False`. `percent_change` is `None` in the same way for percentages, and the two are checked separately, since a filing can report one and not the other. A reported zero stays `0`.
Each filing's figure is its `total_shares` or `total_percent`, so a change between two `'ambiguous'` filings is a change in the largest reporting person's holding.
-`shares_change` and `percent_change` still return a number in 5.x, reading an unreported figure as `0`, and emit a `FutureWarning` when they do. In edgartools 6.0 they return `None` instead, as `reported_shares_change` and `reported_percent_change` do today.
-
-The comparison still adds up the rows of every reporting person in each filing. It does not use `total_shares`, and it does not work out whether joint filers report the same shares, so for a joint filing whose rows overlap the change can be overstated. Check the rows in `reporting_persons` before relying on the change for a joint filing.
+In `get_summary()` a share or percent value is `None` when its filing does not report it. `reported_shares_change` and `reported_percent_change` (and the summary keys of the same names) come from 5.x, where `shares_change` and `percent_change` read an unreported figure as `0`; in 6.0 they equal `shares_change` and `percent_change`.
---
diff --git a/docs/upgrade/6.0.md b/docs/upgrade/6.0.md
index a1cdab0c5..399edda89 100644
--- a/docs/upgrade/6.0.md
+++ b/docs/upgrade/6.0.md
@@ -1355,14 +1355,51 @@ rows and periods, where 5.x could return another statement's.
---
-## Still to come
+## Schedule 13D/G: a figure the filing does not report is `None`, not `0`
+
+**What changed — a quiet one.** An amendment (13D/A, 13G/A) may leave any
+ownership figure off a reporting person's cover page, a pre-2025 filing read
+from its SGML header has none, and a filing can have no reporting-person rows
+at all. 5.x read every such figure as `0`. In 6.0 these are `None` where the
+filing does not report the figure, and a reported `0` stays `0`:
+
+- `ReportingPerson.sole_voting_power`, `shared_voting_power`,
+ `sole_dispositive_power`, `shared_dispositive_power`, `aggregate_amount`,
+ `percent_of_class`
+- `ReportingPerson.total_voting_power` and `total_dispositive_power` (`None`
+ when either half is)
+- `Schedule13D` / `Schedule13G` `total_shares` and `total_percent` (also `None`
+ for a filing with no reporting-person rows; still `0` when every person is
+ flagged `is_aggregate_exclude_shares`)
+- `OwnershipComparison.shares_change`, `percent_change`, and the share and
+ percent values of `get_summary()`
+
+The `FutureWarning` that `shares_change`, `percent_change` and `get_summary()`
+emitted in 5.x is gone. An amendment that omits its share counts no longer
+reads as a sale of the whole position: against BML Investment Partners' 13D on
+Aadi Bioscience (`0001373604-24-000118`, 2,435,000 shares), 5.x reported such
+an amendment as a 2,435,000-share decrease.
-Planned for 6.0 and **not yet merged**. Do not write code against it until it
-has a section above.
+```text
+# 5.x
+amendment.total_shares 0
+comparison.shares_change -2435000 (FutureWarning)
+# 6.0
+amendment.total_shares None
+comparison.shares_change None
+```
-| Change | What to expect |
-|---|---|
-| Schedule 13D/G figures a filing does not report | `ReportingPerson`'s six ownership figures, `total_voting_power`, `total_dispositive_power`, `Schedule13D`/`Schedule13G` `total_shares` and `total_percent`, and `OwnershipComparison.shares_change`, `percent_change` and the share and percent values of `get_summary()` become `None` where the filing does not report the figure, instead of reading it as `0`. 5.x already has the `None`-returning form: `ReportingPerson.reported(figure)`, `ReportingPerson.unreported_fields`, and `OwnershipComparison.reported_shares_change` / `reported_percent_change`; `shares_change` and `percent_change` emit a `FutureWarning` when they read an unreported figure as `0`. A reported `0` stays `0`. |
+**Rewrite rule.** Test for `None` before formatting or arithmetic: `f"{x:,}"`
+and `x + y` raise `TypeError` on `None`. Code already written against the 5.x
+`None`-returning names keeps working unchanged: `ReportingPerson.reported(figure)`,
+`ReportingPerson.unreported_fields`, `OwnershipComparison.reported_shares_change`
+and `reported_percent_change` (and the summary keys of the same names) now equal
+the main properties. If you want the old numbers back, `x or 0` gives them, but a
+`0` you substitute is not one the filing reported.
+
+---
+
+## Not in 6.0
Module moves stop at the ones described above for 6.0. Later moves, such as
the merge of `edgar.markdown` and `edgar._markdown`, come in 6.x with a
diff --git a/edgar/beneficial_ownership/amendments.py b/edgar/beneficial_ownership/amendments.py
index ae4cf65bc..367705836 100644
--- a/edgar/beneficial_ownership/amendments.py
+++ b/edgar/beneficial_ownership/amendments.py
@@ -4,7 +4,6 @@
This module provides utilities for linking amendments to original filings
and comparing ownership changes between filings.
"""
-import warnings
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional
@@ -77,35 +76,6 @@ def _reported_total(schedule: 'Schedule13D | Schedule13G', figure: str):
return _total_over_persons(schedule.reporting_persons, figure)
-def _placeholder_total(schedule: 'Schedule13D | Schedule13G', figure: str):
- """``_reported_total`` reading an unreported figure as 0 (the 5.x values), as ``total_shares`` does."""
- counted = [getattr(p, figure) for p in schedule.reporting_persons if not p.is_aggregate_exclude_shares]
- return max(counted, default=0)
-
-
-def _warn_unreported(name: str, replacement: str) -> None:
- # The text is fixed per property so Python's default filter shows it once per call site.
- # stacklevel=3 skips this helper and the property, so the caller's line is reported.
- warnings.warn(
- f"OwnershipComparison.{name} is computed from figures a filing does not report, read as 0. "
- f"It will be None in edgartools 6.0. Use OwnershipComparison.{replacement}, which is None today.",
- FutureWarning,
- stacklevel=3,
- )
-
-
-def _warn_unreported_summary() -> None:
- # get_summary() warns for itself: reading self.shares_change would attribute the
- # warning to this module, and the default filter would then show it once per process.
- warnings.warn(
- "OwnershipComparison.get_summary() computes 'shares_change' and 'percent_change' from figures "
- "a filing does not report, read as 0. They will be None in edgartools 6.0. "
- "Use the 'reported_shares_change' and 'reported_percent_change' keys, which are None today.",
- FutureWarning,
- stacklevel=3,
- )
-
-
@dataclass
class OwnershipComparison:
"""
@@ -117,25 +87,26 @@ class OwnershipComparison:
A filing's figure is the largest any counted reporting person reports, the
same as ``total_shares``; check each filing's ``aggregation_basis`` before
reading it as the group's holding. A change is known only when both filings
- report the figure for every counted reporting person. An amendment may leave the figures out, a pre-2025 filing
- read from its SGML header has none, and a filing may have no reporting-person
- rows. ``reported_shares_change`` and ``reported_percent_change`` are None then,
- and ``is_accumulating``, ``is_liquidating`` and ``is_unchanged`` are all False.
+ report the figure for every counted reporting person. An amendment may leave the
+ figures out, a pre-2025 filing read from its SGML header has none, and a filing
+ may have no reporting-person rows. ``shares_change`` and ``percent_change`` are
+ None then, and ``is_accumulating``, ``is_liquidating`` and ``is_unchanged`` are
+ all False. A reported 0 is a number.
Example:
original = Schedule13D.from_filing(original_filing)
amendment = Schedule13D.from_filing(amended_filing)
comparison = OwnershipComparison(current=amendment, previous=original)
- if comparison.reported_shares_change is not None:
- print(f"Shares changed by: {comparison.reported_shares_change:,}")
+ if comparison.shares_change is not None:
+ print(f"Shares changed by: {comparison.shares_change:,}")
print(f"Is accumulating: {comparison.is_accumulating}")
"""
current: 'Schedule13D | Schedule13G'
previous: 'Schedule13D | Schedule13G'
@property
- def reported_shares_change(self) -> Optional[int]:
+ def shares_change(self) -> Optional[int]:
"""
Change in total shares owned, or None when either filing does not report its share counts.
@@ -151,13 +122,13 @@ def reported_shares_change(self) -> Optional[int]:
return curr_shares - prev_shares
@property
- def reported_percent_change(self) -> Optional[float]:
+ def percent_change(self) -> Optional[float]:
"""
Change in ownership percentage, or None when either filing does not report its percentages.
Returns:
Net change in ownership percentage (e.g., 1.5 means increased by 1.5%),
- or None as for ``reported_shares_change``
+ or None as for ``shares_change``
"""
curr_pct = _reported_total(self.current, 'percent_of_class')
prev_pct = _reported_total(self.previous, 'percent_of_class')
@@ -166,81 +137,59 @@ def reported_percent_change(self) -> Optional[float]:
return curr_pct - prev_pct
@property
- def shares_change(self) -> int:
- """
- Change in total shares owned.
-
- Returns:
- Net change in share count (positive = increased, negative = decreased).
- A figure a filing does not report counts as 0 here and emits a
- ``FutureWarning``: in edgartools 6.0 this returns None instead, as
- ``reported_shares_change`` does today.
- """
- if self.reported_shares_change is None:
- _warn_unreported('shares_change', 'reported_shares_change')
- return _placeholder_total(self.current, 'aggregate_amount') - _placeholder_total(self.previous, 'aggregate_amount')
+ def reported_shares_change(self) -> Optional[int]:
+ """The same value as ``shares_change``; kept from 5.x, where ``shares_change`` read unreported figures as 0."""
+ return self.shares_change
@property
- def percent_change(self) -> float:
- """
- Change in ownership percentage.
-
- Returns:
- Net change in ownership percentage (e.g., 1.5 means increased by 1.5%).
- A figure a filing does not report counts as 0 here and emits a
- ``FutureWarning``: in edgartools 6.0 this returns None instead, as
- ``reported_percent_change`` does today.
- """
- if self.reported_percent_change is None:
- _warn_unreported('percent_change', 'reported_percent_change')
- return _placeholder_total(self.current, 'percent_of_class') - _placeholder_total(self.previous, 'percent_of_class')
+ def reported_percent_change(self) -> Optional[float]:
+ """The same value as ``percent_change``; kept from 5.x, where ``percent_change`` read unreported figures as 0."""
+ return self.percent_change
@property
def is_accumulating(self) -> bool:
"""Check if shares increased (False when the change is unknown)"""
- change = self.reported_shares_change
+ change = self.shares_change
return change is not None and change > 0
@property
def is_liquidating(self) -> bool:
"""Check if shares decreased (False when the change is unknown)"""
- change = self.reported_shares_change
+ change = self.shares_change
return change is not None and change < 0
@property
def is_unchanged(self) -> bool:
"""Check if shareholding is unchanged (False when the change is unknown)"""
- return self.reported_shares_change == 0
+ return self.shares_change == 0
def get_summary(self) -> dict:
"""
Get summary of changes.
Returns:
- Dictionary with change metrics. The share and percent values read a
- figure a filing does not report as 0, as ``shares_change`` does, and
- emit the same ``FutureWarning``; ``reported_shares_change`` and
- ``reported_percent_change`` are None for them.
+ Dictionary with change metrics. A share or percent value is None when
+ its filing does not report the figure, and a change is None when either
+ side is. ``reported_shares_change`` and ``reported_percent_change`` are
+ kept from 5.x and equal ``shares_change`` and ``percent_change``.
"""
- reported_shares_change = self.reported_shares_change
- reported_percent_change = self.reported_percent_change
- if reported_shares_change is None or reported_percent_change is None:
- _warn_unreported_summary()
- previous_shares = _placeholder_total(self.previous, 'aggregate_amount')
- current_shares = _placeholder_total(self.current, 'aggregate_amount')
- previous_percent = _placeholder_total(self.previous, 'percent_of_class')
- current_percent = _placeholder_total(self.current, 'percent_of_class')
+ previous_shares = _reported_total(self.previous, 'aggregate_amount')
+ current_shares = _reported_total(self.current, 'aggregate_amount')
+ previous_percent = _reported_total(self.previous, 'percent_of_class')
+ current_percent = _reported_total(self.current, 'percent_of_class')
+ shares_change = self.shares_change
+ percent_change = self.percent_change
return {
'previous_filing_date': self.previous.filing_date,
'current_filing_date': self.current.filing_date,
'previous_shares': previous_shares,
'current_shares': current_shares,
- 'shares_change': current_shares - previous_shares,
+ 'shares_change': shares_change,
'previous_percent': previous_percent,
'current_percent': current_percent,
- 'percent_change': current_percent - previous_percent,
- 'reported_shares_change': reported_shares_change,
- 'reported_percent_change': reported_percent_change,
+ 'percent_change': percent_change,
+ 'reported_shares_change': shares_change,
+ 'reported_percent_change': percent_change,
'is_accumulating': self.is_accumulating,
'is_liquidating': self.is_liquidating,
'is_unchanged': self.is_unchanged
diff --git a/edgar/beneficial_ownership/models.py b/edgar/beneficial_ownership/models.py
index 7c48a12d2..228631d59 100644
--- a/edgar/beneficial_ownership/models.py
+++ b/edgar/beneficial_ownership/models.py
@@ -35,10 +35,10 @@ def _reported_total(persons, figure: str) -> Optional[Union[int, float]]:
"""
A filing's total for an ownership figure, or None when a person it counts leaves the figure out.
- The same max() over the persons not flagged ``is_aggregate_exclude_shares`` that
- ``total_shares`` and ``total_percent`` take, which read an unreported figure as 0;
- ``_aggregation_basis`` says whether that max is the group's total. A filing with
- no reporting-person rows reports no figure either.
+ The max() over the persons not flagged ``is_aggregate_exclude_shares`` that
+ ``total_shares`` and ``total_percent`` return; ``_aggregation_basis`` says whether
+ that max is the group's total. A filing with no reporting-person rows reports no
+ figure either. When every person is excluded the total is 0.
"""
if not persons:
return None
@@ -96,32 +96,44 @@ class ReportingPerson:
Represents a single reporting person from the SC 13D or SC 13G filing.
Joint filers will have multiple ReportingPerson instances.
- An ownership figure the filing does not report holds a placeholder 0 and is
- named in ``unreported_fields``; ``reported()`` returns None for it instead.
- A reported 0 stays 0 in both. edgartools 6.0 will make the fields themselves
- None for an unreported figure.
+ An ownership figure the filing does not report is None and is named in
+ ``unreported_fields``; a reported 0 stays 0. An amendment may leave any figure
+ out, and a pre-2025 filing read from its SGML header reports none of them.
+ A figure passed as None is added to ``unreported_fields``, and one named there
+ is set to None, so the two always agree.
"""
cik: str
name: str
citizenship: str
- sole_voting_power: int
- shared_voting_power: int
- sole_dispositive_power: int
- shared_dispositive_power: int
- aggregate_amount: int
- percent_of_class: float
+ sole_voting_power: Optional[int]
+ shared_voting_power: Optional[int]
+ sole_dispositive_power: Optional[int]
+ shared_dispositive_power: Optional[int]
+ aggregate_amount: Optional[int]
+ percent_of_class: Optional[float]
type_of_reporting_person: str
fund_type: Optional[str] = None
comment: Optional[str] = None
member_of_group: Optional[str] = None # "a" = group member (joint filer), "b" = separate filer
is_aggregate_exclude_shares: bool = False # True if shares excluded from aggregate count
no_cik: bool = False # True if reporting person has no CIK assigned
- unreported_fields: FrozenSet[str] = frozenset() # ownership figures the filing leaves out (placeholder 0)
+ unreported_fields: FrozenSet[str] = frozenset() # ownership figures the filing leaves out (None)
+
+ def __post_init__(self):
+ unreported = {figure for figure in self.unreported_fields if figure in _OWNERSHIP_FIGURES}
+ unreported.update(figure for figure in _OWNERSHIP_FIGURES if getattr(self, figure) is None)
+ for figure in unreported:
+ object.__setattr__(self, figure, None)
+ object.__setattr__(self, 'unreported_fields', frozenset(unreported))
def reported(self, figure: str) -> Optional[Union[int, float]]:
"""
An ownership figure as the filing reports it, or None when the filing leaves it out.
+ The same value as the attribute itself (``person.reported('aggregate_amount')``
+ is ``person.aggregate_amount``); kept from 5.x, where the attribute held a
+ placeholder 0, and for looking a figure up by name.
+
Args:
figure: One of ``sole_voting_power``, ``shared_voting_power``,
``sole_dispositive_power``, ``shared_dispositive_power``,
@@ -135,13 +147,17 @@ def reported(self, figure: str) -> Optional[Union[int, float]]:
return getattr(self, figure)
@property
- def total_voting_power(self) -> int:
- """Total voting power (sole + shared)"""
+ def total_voting_power(self) -> Optional[int]:
+ """Total voting power (sole + shared), or None when the filing leaves either out"""
+ if self.sole_voting_power is None or self.shared_voting_power is None:
+ return None
return self.sole_voting_power + self.shared_voting_power
@property
- def total_dispositive_power(self) -> int:
- """Total dispositive power (sole + shared)"""
+ def total_dispositive_power(self) -> Optional[int]:
+ """Total dispositive power (sole + shared), or None when the filing leaves either out"""
+ if self.sole_dispositive_power is None or self.shared_dispositive_power is None:
+ return None
return self.sole_dispositive_power + self.shared_dispositive_power
diff --git a/edgar/beneficial_ownership/schedule13.py b/edgar/beneficial_ownership/schedule13.py
index c85530091..b98d43b85 100644
--- a/edgar/beneficial_ownership/schedule13.py
+++ b/edgar/beneficial_ownership/schedule13.py
@@ -99,8 +99,7 @@ def _ownership_figures(sole_voting: Optional[str], shared_voting: Optional[str],
ReportingPerson keyword arguments for the six ownership figures, from their XML text.
A figure whose element is missing (an amendment may leave any of them out) or
- is not a number keeps the placeholder 0 it has always had, and is named in
- ``unreported_fields`` so that it can be told apart from a reported 0.
+ is not a number is None, and is named in ``unreported_fields``. A reported 0 stays 0.
"""
texts = dict(zip(_OWNERSHIP_FIGURES, (sole_voting, shared_voting, sole_dispositive,
shared_dispositive, aggregate, percent), strict=True))
@@ -111,7 +110,6 @@ def _ownership_figures(sole_voting: Optional[str], shared_voting: Optional[str],
value = parse(text, default=None)
if value is None:
unreported.add(figure)
- value = parse(None)
figures[figure] = value
figures['unreported_fields'] = frozenset(unreported)
return figures
@@ -171,7 +169,7 @@ def _partial_from_header(filing: 'Filing') -> tuple:
identities are recoverable from the header — the subject company (issuer) and
the filer(s)/reporting person(s). Beneficial-ownership numerics
(shares, percentage, voting/dispositive power) are NOT in the header and are
- left as ``0`` / empty; callers must gate on ``has_structured_data``.
+ None, named in ``unreported_fields``; ``has_structured_data`` is False.
Returns:
Tuple of (IssuerInfo, List[ReportingPerson]). Falls back to filing-level
@@ -212,12 +210,12 @@ def _partial_from_header(filing: 'Filing') -> tuple:
cik=ci.cik or '',
name=ci.name,
citizenship='',
- sole_voting_power=0,
- shared_voting_power=0,
- sole_dispositive_power=0,
- shared_dispositive_power=0,
- aggregate_amount=0,
- percent_of_class=0.0,
+ sole_voting_power=None,
+ shared_voting_power=None,
+ sole_dispositive_power=None,
+ shared_dispositive_power=None,
+ aggregate_amount=None,
+ percent_of_class=None,
type_of_reporting_person='',
unreported_fields=frozenset(_OWNERSHIP_FIGURES),
))
@@ -554,24 +552,17 @@ def total_shares(self) -> Optional[int]:
is less than the group's: check ``aggregation_basis`` before treating it as
the group total.
- Excludes shares flagged with is_aggregate_exclude_shares == True.
+ Excludes shares flagged with is_aggregate_exclude_shares == True; 0 when
+ every person is excluded.
- Returns ``None`` when ``has_structured_data`` is False (pre-2025 HTML-only
- filing) — the share count is genuinely unavailable, not zero.
+ Returns ``None`` when the filing does not report the share count: a
+ pre-2025 HTML-only filing (``has_structured_data`` is False), a filing with
+ no reporting-person rows, or an amendment where a counted person leaves
+ its share count out. A reported 0 stays 0.
"""
if not self.has_structured_data:
return None
-
- if not self.reporting_persons:
- return 0
-
- included_persons = [p for p in self.reporting_persons
- if not p.is_aggregate_exclude_shares]
-
- if not included_persons:
- return 0
-
- return max(p.aggregate_amount for p in included_persons)
+ return _reported_total(self.reporting_persons, 'aggregate_amount')
@property
def total_percent(self) -> Optional[float]:
@@ -584,24 +575,15 @@ def total_percent(self) -> Optional[float]:
is less than the group's: check ``aggregation_basis`` before treating it as
the group total.
- Excludes shares flagged with is_aggregate_exclude_shares == True.
+ Excludes shares flagged with is_aggregate_exclude_shares == True; 0 when
+ every person is excluded.
- Returns ``None`` when ``has_structured_data`` is False (pre-2025 HTML-only
- filing) — the percentage is genuinely unavailable, not zero.
+ Returns ``None`` when the filing does not report the percentage, as for
+ ``total_shares``. A reported 0 stays 0.
"""
if not self.has_structured_data:
return None
-
- if not self.reporting_persons:
- return 0.0
-
- included_persons = [p for p in self.reporting_persons
- if not p.is_aggregate_exclude_shares]
-
- if not included_persons:
- return 0.0
-
- return max(p.percent_of_class for p in included_persons)
+ return _reported_total(self.reporting_persons, 'percent_of_class')
@property
def aggregation_basis(self) -> Optional[str]:
@@ -1088,24 +1070,17 @@ def total_shares(self) -> Optional[int]:
is less than the group's: check ``aggregation_basis`` before treating it as
the group total.
- Excludes shares flagged with is_aggregate_exclude_shares == True.
+ Excludes shares flagged with is_aggregate_exclude_shares == True; 0 when
+ every person is excluded.
- Returns ``None`` when ``has_structured_data`` is False (pre-2025 HTML-only
- filing) — the share count is genuinely unavailable, not zero.
+ Returns ``None`` when the filing does not report the share count: a
+ pre-2025 HTML-only filing (``has_structured_data`` is False), a filing with
+ no reporting-person rows, or an amendment where a counted person leaves
+ its share count out. A reported 0 stays 0.
"""
if not self.has_structured_data:
return None
-
- if not self.reporting_persons:
- return 0
-
- included_persons = [p for p in self.reporting_persons
- if not p.is_aggregate_exclude_shares]
-
- if not included_persons:
- return 0
-
- return max(p.aggregate_amount for p in included_persons)
+ return _reported_total(self.reporting_persons, 'aggregate_amount')
@property
def total_percent(self) -> Optional[float]:
@@ -1118,24 +1093,15 @@ def total_percent(self) -> Optional[float]:
is less than the group's: check ``aggregation_basis`` before treating it as
the group total.
- Excludes shares flagged with is_aggregate_exclude_shares == True.
+ Excludes shares flagged with is_aggregate_exclude_shares == True; 0 when
+ every person is excluded.
- Returns ``None`` when ``has_structured_data`` is False (pre-2025 HTML-only
- filing) — the percentage is genuinely unavailable, not zero.
+ Returns ``None`` when the filing does not report the percentage, as for
+ ``total_shares``. A reported 0 stays 0.
"""
if not self.has_structured_data:
return None
-
- if not self.reporting_persons:
- return 0.0
-
- included_persons = [p for p in self.reporting_persons
- if not p.is_aggregate_exclude_shares]
-
- if not included_persons:
- return 0.0
-
- return max(p.percent_of_class for p in included_persons)
+ return _reported_total(self.reporting_persons, 'percent_of_class')
@property
def is_passive_investor(self) -> bool:
diff --git a/tests/issues/regression/test_issue_1379_schedule13_unreported_figures.py b/tests/issues/regression/test_issue_1379_schedule13_unreported_figures.py
index 0df58d672..dc677ea8b 100644
--- a/tests/issues/regression/test_issue_1379_schedule13_unreported_figures.py
+++ b/tests/issues/regression/test_issue_1379_schedule13_unreported_figures.py
@@ -23,13 +23,13 @@
The parsers now record which figures a filing leaves out
(``ReportingPerson.unreported_fields``, ``ReportingPerson.reported()``) and the
comparison treats them as unknown: ``reported_shares_change`` and
-``reported_percent_change`` are None and no direction flag is set. The 5.x
-fields keep their types, so an unreported figure still holds the placeholder 0
-and ``shares_change``/``percent_change`` still return numbers, with a
-FutureWarning that they become None in 6.0 (the 6.0 release plan stages value
-changes additively in 5.x). A reported 0 stays 0: the controls below check that
-selling out, a single-person sale and a 13G/A that does report its figures
-still compare as numbers, without a warning.
+``reported_percent_change`` are None and no direction flag is set. In 5.x the
+fields kept a placeholder 0 and ``shares_change``/``percent_change`` returned
+numbers with a FutureWarning; edgartools 6.0 made them None (edgartools-u6tkw),
+so the assertions on those placeholders now assert None and no warning. A
+reported 0 stays 0: the controls below check that selling out, a single-person
+sale and a 13G/A that does report its figures still compare as numbers, without
+a warning.
"""
import re
import warnings
@@ -82,18 +82,18 @@ def _header_only_schedule13d():
return Schedule13D.from_filing(filing)
-def _assert_change_unknown(comparison, placeholder_shares_change):
+def _assert_change_unknown(comparison):
"""No purchase, sale or "unchanged" is read into a comparison with unreported figures."""
- assert comparison.is_accumulating is False
- assert comparison.is_liquidating is False
- assert comparison.is_unchanged is False
- assert comparison.reported_shares_change is None
- assert comparison.reported_percent_change is None
- # 5.x keeps the numeric properties, counting an unreported figure as 0, and says so.
- with pytest.warns(FutureWarning, match=r'shares_change .* None in edgartools 6\.0'):
- assert comparison.shares_change == placeholder_shares_change
- with pytest.warns(FutureWarning, match=r'percent_change .* None in edgartools 6\.0'):
- assert isinstance(comparison.percent_change, float)
+ with warnings.catch_warnings():
+ warnings.simplefilter('error', FutureWarning)
+ assert comparison.is_accumulating is False
+ assert comparison.is_liquidating is False
+ assert comparison.is_unchanged is False
+ assert comparison.reported_shares_change is None
+ assert comparison.reported_percent_change is None
+ # 6.0: the main properties are None too (5.x read the figure as 0 and warned).
+ assert comparison.shares_change is None
+ assert comparison.percent_change is None
@pytest.mark.fast
@@ -107,17 +107,17 @@ def test_ownership_comparison_against_header_only_filing_is_unknown():
structured = _schedule13d_from_xml(SCHEDULE_13D_XML_PATH.read_text())
assert header_only.has_structured_data is False
- _assert_change_unknown(OwnershipComparison(current=structured, previous=header_only), 2_435_000)
- _assert_change_unknown(OwnershipComparison(current=header_only, previous=structured), -2_435_000)
+ _assert_change_unknown(OwnershipComparison(current=structured, previous=header_only))
+ _assert_change_unknown(OwnershipComparison(current=header_only, previous=structured))
person = header_only.reporting_persons[0]
assert person.unreported_fields == OWNERSHIP_FIGURES
assert person.reported('aggregate_amount') is None
- with pytest.warns(FutureWarning):
- summary = OwnershipComparison(current=structured, previous=header_only).get_summary()
+ summary = OwnershipComparison(current=structured, previous=header_only).get_summary()
assert summary['reported_shares_change'] is None
assert summary['reported_percent_change'] is None
+ assert summary['shares_change'] is None
assert summary['is_accumulating'] is False
@@ -132,34 +132,37 @@ def test_ownership_comparison_without_reporting_person_rows_is_unknown():
assert no_rows.has_structured_data is True
assert no_rows.reporting_persons == []
- _assert_change_unknown(OwnershipComparison(current=no_rows, previous=structured), -2_435_000)
- _assert_change_unknown(OwnershipComparison(current=structured, previous=no_rows), 2_435_000)
+ _assert_change_unknown(OwnershipComparison(current=no_rows, previous=structured))
+ _assert_change_unknown(OwnershipComparison(current=structured, previous=no_rows))
+ assert no_rows.total_shares is None
+ assert no_rows.total_percent is None
@pytest.mark.fast
-def test_unreported_figures_keep_their_5x_types():
- """Control: code written against 5.x keeps getting numbers until 6.0.
+def test_unreported_figures_are_none():
+ """6.0: an unreported figure is None in the field itself, not the 5.x placeholder 0.
- Formatting a figure of a header-only filing, or a change against it, printed
- 0 and a number before this fix and must not raise TypeError now. Only the new
- accessors answer None.
+ This test asserted the 5.x placeholders (0, and a change of +2,435,000 against
+ a header-only filing) until edgartools-u6tkw made them None.
"""
header_only = _header_only_schedule13d()
structured = _schedule13d_from_xml(SCHEDULE_13D_XML_PATH.read_text())
person = header_only.reporting_persons[0]
- assert format(person.aggregate_amount, ',') == '0'
- assert f"{person.percent_of_class:.2f}%" == '0.00%'
- assert person.total_voting_power == 0
- assert person.total_dispositive_power == 0
+ for field_name in OWNERSHIP_FIGURES:
+ assert getattr(person, field_name) is None
+ assert person.total_voting_power is None
+ assert person.total_dispositive_power is None
with warnings.catch_warnings():
- warnings.simplefilter('ignore', FutureWarning)
+ warnings.simplefilter('error', FutureWarning)
comparison = OwnershipComparison(current=structured, previous=header_only)
- assert f"{comparison.shares_change:+,}" == '+2,435,000'
- assert f"{comparison.percent_change:+.1f}" == '+9.9'
+ assert comparison.shares_change is None
+ assert comparison.percent_change is None
summary = comparison.get_summary()
- assert summary['previous_shares'] == 0
+ assert summary['previous_shares'] is None
+ assert summary['previous_percent'] is None
assert summary['current_shares'] == 2_435_000
+ assert summary['current_percent'] == 9.9
@pytest.mark.fast
@@ -272,7 +275,7 @@ def test_amendment_omitting_ownership_figures_marks_them_unreported(schedule_cla
assert person.unreported_fields == OWNERSHIP_FIGURES
for field_name in OWNERSHIP_FIGURES:
assert person.reported(field_name) is None
- assert getattr(person, field_name) == 0 # the 5.x placeholder, None in 6.0
+ assert getattr(person, field_name) is None # the 5.x placeholder 0 became None in 6.0
# The display paths show the gap instead of a zero.
context = amendment.to_context(detail='full')
@@ -373,11 +376,10 @@ def test_ownership_comparison_13d_amendment_omitting_figures_is_unknown():
assert amendment.has_structured_data is True
assert len(amendment.reporting_persons) == 2
- _assert_change_unknown(OwnershipComparison(current=amendment, previous=original), -2_435_000)
- _assert_change_unknown(OwnershipComparison(current=original, previous=amendment), 2_435_000)
+ _assert_change_unknown(OwnershipComparison(current=amendment, previous=original))
+ _assert_change_unknown(OwnershipComparison(current=original, previous=amendment))
- with pytest.warns(FutureWarning):
- summary = OwnershipComparison(current=amendment, previous=original).get_summary()
+ summary = OwnershipComparison(current=amendment, previous=original).get_summary()
assert summary['is_liquidating'] is False
assert summary['reported_shares_change'] is None
assert summary['reported_percent_change'] is None
@@ -393,8 +395,8 @@ def test_ownership_comparison_13g_amendment_omitting_figures_is_unknown():
assert amendment.has_structured_data is True
assert len(amendment.reporting_persons) == 2
- _assert_change_unknown(OwnershipComparison(current=amendment, previous=original), -10_000_000)
- _assert_change_unknown(OwnershipComparison(current=original, previous=amendment), 10_000_000)
+ _assert_change_unknown(OwnershipComparison(current=amendment, previous=original))
+ _assert_change_unknown(OwnershipComparison(current=original, previous=amendment))
@pytest.mark.fast
@@ -421,12 +423,11 @@ def test_ownership_comparison_13g_amendment_reporting_figures():
@pytest.mark.fast
-def test_get_summary_warns_once_at_the_callers_line():
- """get_summary() names the caller's line, not amendments.py.
+def test_get_summary_emits_no_warning():
+ """6.0: get_summary() reports an unreported figure as None and warns no more.
- It used to read self.shares_change and self.percent_change, so it emitted two
- warnings attributed to amendments.py, and Python's default filter then showed
- them once per process no matter where get_summary() was called from.
+ In 5.x it emitted one FutureWarning at the caller's line and returned the
+ placeholder values (shares_change 2,435,000, previous_shares 0).
"""
comparison = OwnershipComparison(current=_schedule13d_from_xml(SCHEDULE_13D_XML_PATH.read_text()),
previous=_header_only_schedule13d())
@@ -434,14 +435,10 @@ def test_get_summary_warns_once_at_the_callers_line():
warnings.simplefilter('always')
summary = comparison.get_summary()
- future = [w for w in caught if issubclass(w.category, FutureWarning)]
- assert len(future) == 1
- assert future[0].filename == __file__
- assert "'reported_shares_change'" in str(future[0].message)
- # The 5.x values are unchanged.
- assert summary['shares_change'] == 2_435_000
+ assert [w for w in caught if issubclass(w.category, FutureWarning)] == []
+ assert summary['shares_change'] is None
assert summary['current_shares'] == 2_435_000
- assert summary['previous_shares'] == 0
+ assert summary['previous_shares'] is None
assert summary['reported_shares_change'] is None
diff --git a/tests/issues/regression/test_u6tkw_schedule13_unreported_is_none.py b/tests/issues/regression/test_u6tkw_schedule13_unreported_is_none.py
new file mode 100644
index 000000000..50ae3c084
--- /dev/null
+++ b/tests/issues/regression/test_u6tkw_schedule13_unreported_is_none.py
@@ -0,0 +1,188 @@
+"""Schedule 13D/G figures a filing does not report are None, not 0 (edgartools-u6tkw, 6.0).
+
+Bead: edgartools-u6tkw. Follows GitHub PR https://github.com/dgunning/edgartools/pull/1379,
+which added ``ReportingPerson.reported()``/``unreported_fields`` and
+``OwnershipComparison.reported_shares_change``/``reported_percent_change`` in 5.x
+and warned that the main properties would become None in 6.0.
+
+In 5.x an unreported figure held a placeholder 0 in ``ReportingPerson``'s six
+ownership fields, ``total_voting_power`` and ``total_dispositive_power`` added the
+placeholders up, ``total_shares``/``total_percent`` returned 0 for a filing with no
+reporting-person rows, and ``OwnershipComparison.shares_change``/``percent_change``
+and ``get_summary()`` computed with the placeholders behind a FutureWarning. In 6.0
+they are None, and a reported 0 stays 0.
+
+Ground truth: BML Investment Partners' SCHEDULE 13D on Aadi Bioscience, Inc.,
+accession 0001373604-24-000118, filed 2024-12-31 (event date 12/31/2024). The
+fixture tests/data/beneficial_ownership/schedule13d.xml is that filing's primary
+XML (identical to SEC's apart from whitespace and the masked filer CCC). Its cover
+pages report:
+
+- BML Investment Partners, L.P.: sole voting 0, shared voting 2,100,000,
+ sole dispositive 0, shared dispositive 2,100,000, aggregate 2,100,000, 8.5%
+- Leonard Braden Michael: sole voting 335,000, shared voting 2,100,000,
+ sole dispositive 335,000, shared dispositive 2,100,000, aggregate 2,435,000, 9.9%
+"""
+import re
+import warnings
+from datetime import date
+from pathlib import Path
+from unittest.mock import Mock
+
+import pytest
+
+from edgar.beneficial_ownership import ReportingPerson, Schedule13D
+from edgar.beneficial_ownership.amendments import OwnershipComparison
+from edgar.exceptions import ValidationError
+
+FIXTURE = Path(__file__).resolve().parents[2] / "data" / "beneficial_ownership" / "schedule13d.xml"
+ACCESSION = "0001373604-24-000118"
+
+
+def _schedule(xml_content, form='SCHEDULE 13D', filing_date=date(2024, 12, 31)):
+ filing = Mock(form=form, filing_date=filing_date, accession_no=ACCESSION, xml=Mock(return_value=xml_content))
+ return Schedule13D.from_filing(filing)
+
+
+def _header_only():
+ filer = Mock()
+ filer.company_information.name = 'BML Investment Partners, L.P.'
+ filer.company_information.cik = '0001373604'
+ filing = Mock(form='SC 13D', filing_date=date(2021, 3, 1), xml=Mock(return_value=None),
+ header=Mock(filers=[filer], subject_companies=[]))
+ filing.cik = 1422142
+ filing.company = 'Aadi Bioscience, Inc.'
+ return Schedule13D.from_filing(filing)
+
+
+@pytest.fixture
+def no_future_warnings():
+ with warnings.catch_warnings():
+ warnings.simplefilter('error', FutureWarning)
+ yield
+
+
+@pytest.mark.fast
+def test_reported_figures_keep_their_filed_values(no_future_warnings):
+ """Every figure 0001373604-24-000118 reports, including BML's reported 0 sole voting power."""
+ schedule = _schedule(FIXTURE.read_text())
+ bml, leonard = schedule.reporting_persons
+ assert bml.name == 'BML Investment Partners, L.P.'
+ assert bml.unreported_fields == frozenset()
+ assert bml.sole_voting_power == 0 # a reported 0 stays 0
+ assert bml.sole_dispositive_power == 0
+ assert bml.shared_voting_power == 2_100_000
+ assert bml.aggregate_amount == 2_100_000
+ assert bml.percent_of_class == 8.5
+ assert bml.total_voting_power == 2_100_000
+ assert bml.total_dispositive_power == 2_100_000
+
+ assert leonard.name == 'Leonard Braden Michael'
+ assert leonard.sole_voting_power == 335_000
+ assert leonard.aggregate_amount == 2_435_000
+ assert leonard.percent_of_class == 9.9
+ assert leonard.total_voting_power == 2_435_000
+ assert leonard.total_dispositive_power == 2_435_000
+
+ assert schedule.total_shares == 2_435_000
+ assert schedule.total_percent == 9.9
+
+
+@pytest.mark.fast
+def test_figure_left_out_is_none(no_future_warnings):
+ """An amendment leaving out BML's share count and shared voting power: those are None."""
+ xml = FIXTURE.read_text()
+ xml = xml.replace('2100000', '', 1)
+ xml = xml.replace('2100000', '', 1)
+ amendment = _schedule(xml, form='SCHEDULE 13D/A', filing_date=date(2025, 2, 4))
+ bml, leonard = amendment.reporting_persons
+
+ assert bml.unreported_fields == {'aggregate_amount', 'shared_voting_power'}
+ assert bml.aggregate_amount is None
+ assert bml.shared_voting_power is None
+ assert bml.total_voting_power is None # 5.x: 0 + 0 = 0
+ assert bml.sole_voting_power == 0 # still reported as 0
+ assert bml.total_dispositive_power == 2_100_000 # both halves reported
+ assert bml.percent_of_class == 8.5
+ assert bml.reported('aggregate_amount') is None # the 5.x accessor agrees
+ assert bml.reported('sole_voting_power') == 0
+ assert leonard.aggregate_amount == 2_435_000
+
+ # A counted person leaves its shares out, so the filing's total is unknown;
+ # every person reports a percent, so that total is known.
+ assert amendment.total_shares is None
+ assert amendment.total_percent == 9.9
+
+ comparison = OwnershipComparison(current=amendment, previous=_schedule(FIXTURE.read_text()))
+ assert comparison.shares_change is None
+ assert comparison.reported_shares_change is None
+ assert comparison.percent_change == 0.0
+ assert comparison.reported_percent_change == 0.0
+ assert comparison.is_accumulating is False
+ assert comparison.is_liquidating is False
+ assert comparison.is_unchanged is False
+
+ summary = comparison.get_summary()
+ assert summary['current_shares'] is None
+ assert summary['previous_shares'] == 2_435_000
+ assert summary['shares_change'] is None
+ assert summary['current_percent'] == 9.9
+ assert summary['percent_change'] == 0.0
+
+
+@pytest.mark.fast
+def test_header_only_and_no_rows_report_nothing(no_future_warnings):
+ """A pre-2025 header-only filing and an empty have no figures."""
+ header_only = _header_only()
+ assert header_only.has_structured_data is False
+ person = header_only.reporting_persons[0]
+ assert person.aggregate_amount is None
+ assert person.percent_of_class is None
+ assert person.total_voting_power is None
+ assert header_only.total_shares is None
+ assert header_only.total_percent is None
+
+ no_rows = _schedule(re.sub(r'.*?', '',
+ FIXTURE.read_text(), flags=re.S))
+ assert no_rows.reporting_persons == []
+ assert no_rows.total_shares is None # 5.x: 0
+ assert no_rows.total_percent is None # 5.x: 0.0
+
+ comparison = OwnershipComparison(current=_schedule(FIXTURE.read_text()), previous=header_only)
+ assert comparison.shares_change is None # 5.x: +2,435,000 with a FutureWarning
+ assert comparison.percent_change is None
+ assert comparison.get_summary()['previous_shares'] is None
+
+
+@pytest.mark.fast
+def test_reported_zero_compares_as_a_number(no_future_warnings):
+ """Selling out to a reported 0 is a 2,435,000-share decrease, not an unknown."""
+ xml = FIXTURE.read_text()
+ sold_out_xml = xml.replace('2100000',
+ '0')
+ sold_out_xml = sold_out_xml.replace('2435000',
+ '0')
+ sold_out = _schedule(sold_out_xml, form='SCHEDULE 13D/A', filing_date=date(2025, 6, 1))
+ assert sold_out.total_shares == 0
+ comparison = OwnershipComparison(current=sold_out, previous=_schedule(xml))
+ assert comparison.shares_change == -2_435_000
+ assert comparison.is_liquidating is True
+
+
+@pytest.mark.fast
+def test_constructor_keeps_none_and_unreported_fields_in_step():
+ """A figure passed as None is named in unreported_fields, and one named there is None."""
+ common = dict(cik='0001373604', name='BML Investment Partners, L.P.', citizenship='DE',
+ sole_voting_power=0, shared_voting_power=2_100_000, sole_dispositive_power=0,
+ shared_dispositive_power=2_100_000, type_of_reporting_person='PN')
+ from_none = ReportingPerson(aggregate_amount=None, percent_of_class=8.5, **common)
+ assert from_none.unreported_fields == {'aggregate_amount'}
+
+ # 5.x-style construction: a placeholder 0 named in unreported_fields
+ from_placeholder = ReportingPerson(aggregate_amount=0, percent_of_class=8.5,
+ unreported_fields=frozenset({'aggregate_amount'}), **common)
+ assert from_placeholder.aggregate_amount is None
+ assert from_placeholder == from_none
+
+ with pytest.raises(ValidationError):
+ from_none.reported('cik')