Mypy is not able to find an attribute defined in the parent NamedTuple

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)
Rate this post
We use cookies in order to give you the best possible experience on our website. By continuing to use this site, you agree to our use of cookies.
Accept
Reject