Skip to content

Dict's .get() method doesn't limit possible types of the given element #12457

Open
@mcAnswer

Description

@mcAnswer

Bug Report

from typing import Optional, Dict

def test(arg: str) -> None:
    ...

def this_fails(arg: Dict[str, Optional[str]]) -> None:
    if arg.get("key") is not None:
       test(arg["key"])  # error

def this_fails_also(arg: Dict[str, Optional[str]]) -> None:
    if arg.get("key") is not None:
       test(arg.get("key"))  # error

def but_this_pass(arg: Dict[str, Optional[str]]) -> None:
    if arg.get("key") and arg["key"] is not None:
        test(arg["key"])

def this_pass_also_but_is_unsafe(arg: Dict[str, Optional[str]]) -> None:
    if arg["key"] is not None:
        test(arg["key"])

First two methods fail due to Argument 1 to "test" has incompatible type "Optional[str]"; expected "str" error.
Mypy seems to see no relation between arg["key"] and arg.get("key") and it should (IMHO).
I'm not sure if this is a bug and not a feature request, but arg.get("key") and arg["key"] is not None looks redundant for me.

Expected Behavior
The possibility of arg["key"] is None should be excluded by if arg.get("key") is not None because the inner block is unreachable if "key in arg and arg["key"] is None so the value is no longer Optional.

Environment
mypy 0.942
Python 3.10.0

Activity

DevilXD

DevilXD commented on Mar 26, 2022

@DevilXD

There's no type-narrowing done via the get method, so this is more of a feature request rather than a bug. Calling get will always return Optional[...] / ... | None and getting the value via key lookup always returns just the value type, no matter if it's actually in the dictionary or not. In my opinion, using the get method to narrow the type here is completely unnecessary and leads to general code clutter. If you need to call some test function depending on some value existing in the dictionary (like in the examples you've provided), there's two approaches:

# with 'get'
value = arg.get("key")
if value is not None:
    test(value)

# with key containment test
if "key" in arg:
    test(arg["key"])

# bonus: 'get' with a valrus operator
if (value := arg.get("key")) is not None:
    test(value)

Both are easy to read, perfectly valid code and type-check just fine.

spikethehobbitmage

spikethehobbitmage commented on Jan 17, 2025

@spikethehobbitmage

@DevilXD
Your first and third examples are more cluttery than OP's first example, and the second doesn't (and shouldn't) pass because arg["key"] can be None. It should read if "key" in arg and arg["key"] is not None:, which is also more cluttery than necessary.

DevilXD

DevilXD commented on Jan 17, 2025

@DevilXD

Your first and third examples are more cluttery than OP's first example

I don't think so, as my code only does exactly one lookup in every case, only with an extra containment test in the 2nd example. Meanwhile, OP has double the lookups in each of their examples, and even triple in the 3rd case - that's way more than necessary. I'd say that's the real clutter right there.

the second doesn't (and shouldn't) pass because arg["key"] can be None. It should read if "key" in arg and arg["key"] is not None:

That's correct. The main point of my examples was to show that the get method does no type-narrowing, and thus one should use either of those constructs (or something similar) to avoid the errors mentioned in this issue. I personally usually go with the 3rd option myself.

spikethehobbitmage

spikethehobbitmage commented on Jan 18, 2025

@spikethehobbitmage

Your first and third examples are more cluttery than OP's first example

I don't think so, as my code only does exactly one lookup in every case, only with an extra containment test in the 2nd example. Meanwhile, OP has double the lookups in each of their examples, and even triple in the 3rd case - that's way more than necessary. I'd say that's the real clutter right there.

I find the OP's first example to be easier to read, and it doesn't clutter the namespace with a temporary variable. OP's third example was an example of an undesirable workaround.

the second doesn't (and shouldn't) pass because arg["key"] can be None. It should read if "key" in arg and arg["key"] is not None:

That's correct. The main point of my examples was to show that the get method does no type-narrowing, and thus one should use either of those constructs (or something similar) to avoid the errors mentioned in this issue. I personally usually go with the 3rd option myself.

That get doesn't do type narrowing isn't in question, but whether or not it should. At present mypy is flagging valid code as invalid, which seems wrong.

Another example:

args:dict[str, str|None] = dict()
keys:list[str] = list()
selected:list[str] = [args[key] for key in keys if args.get(key) is not None]

This could be accomplished with a nested comprehension, but that adds unnecessary complexity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @JelleZijlstra@DevilXD@mcAnswer@AlexWaygood@spikethehobbitmage

        Issue actions

          Dict's .get() method doesn't limit possible types of the given element · Issue #12457 · python/mypy