Use of ```not``` keyword does not always narrow out type ```None```
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- PR merge metrics
- PR metrics pending
Description
Bug Report
I wanted to make an item class with multiple descriptor-'variant' attributes (optional-strings). Any of these attr's could be null/empty, but AT LEAST ONE of them needs to be defined (i.e. a non-empty string). And I want a read-only property that should return the 'primary' descriptor-value (as string), or raise an err if no descriptors are defined.
In the simplified example below, I implemented this logic for two descriptor-variants, in two seemingly-equivalent ways, but for some reason MyPy yields an error in the first approach but not the second.
To Reproduce
from typing import Optional
class Item_V1:
descriptor_variant1: Optional[str]
descriptor_variant2: Optional[str]
# ... followed by any number of other arbitrary attr's that we don't care about.
@property
def primary_descriptor(self) -> str:
if not self.descriptor_variant1 and not self.descriptor_variant2:
raise AttributeError("At least one item-descriptor variant must be defined.")
if not self.descriptor_variant1:
return self.descriptor_variant2
return self.descriptor_variant1
class Item_V2:
descriptor_variant1: Optional[str]
descriptor_variant2: Optional[str]
@property
def primary_descriptor(self) -> str:
if not self.descriptor_variant1:
if not self.descriptor_variant2:
raise AttributeError("At least one item-descriptor variant must be defined.")
return self.descriptor_variant2
return self.descriptor_variant1
MyPy Output
I believe that the primary_descriptor property for both Item_V1 and Item_V2 should be evaluated without errors by MyPy; however, in reality, l.13 in the Item_V1 approach yields MyPy error "Incompatible return value type (got "Optional[str]", expected "str") [return-value]", whereas the equivalent return-statement for Item_V2 (l.26) is evaluated as correct.
Your Environment
- Python version used: 3.9 [but tested up to v3.12 using https://mypy-play.net/]
- Mypy version used: 1.6.1 [but tested up to v1.9.0 using https://mypy-play.net/]
- Mypy command-line flags: N/A
- Mypy configuration options from
mypy.ini(and other config files): nothing relevant
Caveats/Disclaimers
- Whilst in theory the
Item_V2method is fine for my current scenario, in an ideal world I would prefer to be able to the use theItem_V1-type sequence, as otherwise this would lead to an n-level nested if-statement, which seems like bad style, and would become unwieldy as num. descriptor variants increases. And mostly I just don't understand why there is any difference in MyPy's behaviour... - Before anyone asks something of the form: why not store the descriptor variants using an Iterable/dict? - the actual use-case here is in a Django model, where each descriptor variant needs to map to a nullable/blank-able CharField. But that's not really relevant to the error state, in any case.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the Item_V1 and Item_V2 snippets in mypy-play.net using the reported Python and mypy versions, then trace the type-narrowing behavior for chained not checks. Done means the discrepancy is confirmed and the expected handling is covered by a regression test.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100