Mypy is not able to find an attribute defined in the parent NamedTuple
Problem Description:
In my project I’m using Fava. Fava, is using Beancount. I have configured Mypy
to read the stubs locally by setting mypy_path
in mypy.ini
. Mypy
is able to read the config. So far so good.
Consider this function of mine
1 def get_units(postings: list[Posting]):
2 numbers = []
3 for posting in postings:
4 numbers.append(posting.units.number)
5 return numbers
When I run mypy src
I get the following error
report.py:4 error: Item "type" of "Union[Amount, Type[MISSING]]" has no attribute "number" [union-attr]
When I check the defined stub here I can see the type of units
which is Amount
. Now, Amount
is inheriting number
from its parent _Amount
. Going back to the stubs in Fava
I can see the type here.
My question is why mypy
is not able to find the attribute number
although it is defined in the stubs?
Solution – 1
The type of units
isn’t Amount
:
class Posting(NamedTuple):
account: Account
units: Union[Amount, Type[MISSING]]
It’s Union[Amount, Type[MISSING]]
, exactly like the error message says. And if it’s Type[MISSING]
there is no number
attribute, exactly like the error message says. If you were to run this code and units
were in fact MISSING
, it would raise an AttributeError
on trying to access that number
attribute.
(Aside: I’m not familiar with beancount
, but this seems like a weird interface to me — the more idiomatic thing IMO would be to just make it an Optional[Amount]
with None
representing the "missing" case.)
You need to change your code to account for the MISSING
possibility so that mypy
knows you’re aware of it (and also so that readers of your code can see that possibility before it bites them in the form of an AttributeError
). Something like:
for posting in postings:
assert isinstance(posting.units, Amount), "Units are MISSING!"
numbers.append(posting.units.number)
Obviously if you want your code to do something other than raise an AssertionError
on MISSING
units
, you should write your code to do that instead of assert
.
If you want to just assume that it’s an Amount
and raise an AttributeError
at runtime if it’s not, use typing.cast
to tell mypy that you want to consider it to be an Amount
even though the type stub says otherwise:
for posting in postings:
# posting.units might be MISSING, but let's assume it's not
numbers.append(cast(Amount, posting.units).number)