python / python/mypy

Use of ```not``` keyword does not always narrow out type ```None```

Open
#17,107 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug topic-type-narrowing
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_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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.