Search code examples
pythonpython-typingmypy

How to force cast typed dicts to avoid Incompatible types in assignment


I am trying to do the following (simplified example) in a high efficiency function (trying to avoid copy), without using NotRequired (for reasons shown after):

Mytd1 = TypedDict("Mytd1", {a: NotRequired[str], x: int})
Mytd_extra = TypedDict("Mytd_extra", {a: str, x: int, z: str})


def foo(y: Mytd1) -> Mytd_extra:
    # users input object should NOT contain z
    y = cast(Mytd_extra, y). # fails - my guess is `a` is the offender
    y["z"] = "hooray"
    if "a" not in y:
        y["a"] = "whatever"
    return y
 

Mypy complains about the cast as "incompatible types in assignment", but it's what I want to do. How do I achieve this? I think a is the offender - it's required on the output type

I do not want:

# kill Mytd1 
Mytd_extra = TypedDict("Mytd_extra", {x: int, z: NotRequired[str]})

because this would allow the client to pass in {x: 5, z: "asdf"} into foo but that should be forbidden - they are only allowed to pass {x: 5}.

I think the following would be doable but I'm trying to avoid it for efficiency as this function is called many times:

def foo(y: Mytd1) -> Mytd_extra:
    new_var: Mytd_extra = {k: v for k:v in y.items()}
    new_var["z"] = "ohno"
    return new_var

Solution

  • You have at least two completely valid solutions (assuming python 3.11 not to bother with typing vs typing_extensions, adjust your imports if needed). The problem is that y is of type Mytd1, so you cannot assign Mytd_extra (result of cast) to it.

    Use another variable

    from typing import TypedDict, NotRequired, cast
    
    Mytd1 = TypedDict("Mytd1", {'a': NotRequired[str], 'x': int})
    Mytd_extra = TypedDict("Mytd_extra", {'a': str, 'x': int, 'z': str})
    
    
    def foo(orig_y: Mytd1) -> Mytd_extra:
        # users input object should NOT contain z
        y = cast(Mytd_extra, orig_y)
        y["z"] = "hooray"
        if "a" not in y:
            y["a"] = "whatever"
        return y
    

    Here is a playground with this solution.

    Allow redefinition (mypy flag)

    Preserve your original code, but run with --allow-redefinition (or add allow_redefinition = true to config file you use): playground.

    Warning: allow_redefinition does not have Super Cow Powers, it's use case is limited to a single scenario: assignment to a pre-defined variable, when expression in RHS includes this variable. It allows the following:

    y = 'abc'
    y = list(y)
    

    but not this:

    y = 'abc'
    y = 1