Skip to content

Use of not keyword does not always narrow out type None #17107

@stephenskett

Description

@stephenskett

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_V2 method is fine for my current scenario, in an ideal world I would prefer to be able to the use the Item_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-type-narrowingConditional type narrowing / binder

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions