Search code examples
pythonmypy

Mypy - Incompatible types in assignment when assigning different values in alternative branches


I am wondering why I am getting Incompatible types in assignment here?

from typing import cast

import pyvisa
from pyvisa.constants import InterfaceType
from pyvisa.resources import GPIBInstrument, TCPIPInstrument


class Instrument:
    resource_manager = pyvisa.ResourceManager()

    def __init__(self, resource: str):
        self.resource = self.resource_manager.open_resource(resource_name=resource)
        if self.resource.interface_type == InterfaceType.tcpip:
            self.instance: TCPIPInstrument = cast(TCPIPInstrument, resource)
        elif self.resource.interface_type == InterfaceType.gpib:
            self.instance: GPIBInstrument = cast(GPIBInstrument, resource)
        else:
            raise TypeError(f"Unsupported resource interface type: {self.resource.interface_type}")

Gives Incompatible types in assignment (expression has type "GPIBInstrument", variable has type "TCPIPInstrument")

self.instance gets correctly the type instance: TCPIPInstrument | GPIBInstrument in vscode.

I am using python 3.11.3 and mypy 1.2.0.

Link to a gist with the same issue, but with slightly different code since I could not get pyvisa to install in the playground.

Found some issues in the code from the comments, here is the more correct code, but still with the same issue.

from typing import cast

import pyvisa
from pyvisa.constants import InterfaceType
from pyvisa.resources import GPIBInstrument, TCPIPInstrument


class Instrument:
    resource_manager = pyvisa.ResourceManager()

    def __init__(self, resource_name: str):
        self.resource = self.resource_manager.open_resource(resource_name=resource_name)
        if self.resource.interface_type == InterfaceType.tcpip:
            self.instance = cast(TCPIPInstrument, self.resource)
        elif self.resource.interface_type == InterfaceType.gpib:
            self.instance = cast(GPIBInstrument, self.resource)
        else:
            raise TypeError(f"Unsupported resource interface type: {self.resource.interface_type}")

Solution

  • self.instance can only have one type, which is bound when it's first declared; mypy won't widen a variable's type when you assign a new value to it (indeed, part of the point of declaring a type is to keep you from accidentally mixing types for a single variable in different logic branches). It sounds like the VSCode plugin's typechecker does something different and it automatically infers the type as a union; it'd presumably do the same thing if you did something like:

    foo = "123"  # type is str
    foo = 123    # type is now str|int I guess?
    

    For mypy's purposes, if you want the variable's type to be a union, you need to specify that explicitly when it's first declared:

            if self.resource.interface_type == InterfaceType.tcpip:
                self.instance: GPIBInstrument | TCPIPInstrument = cast(TCPIPInstrument, resource)
            elif self.resource.interface_type == InterfaceType.gpib:
                self.instance = cast(GPIBInstrument, resource)
            else:
                raise TypeError(f"Unsupported resource interface type: {self.resource.interface_type}")
    

    Since the variable will be bound to the same union type regardless of which of the two actual types it has at runtime, you can remove some code duplication by just doing a single cast to that type:

    if self.resource.interface_type in (InterfaceType.tcpip, InterfaceType.gpib):
        self.instance = cast(GPIBInstrument | TCPIPInstrument, self.resource)
    else:
        raise TypeError(f"Unsupported resource interface type: {self.resource.interface_type}")