Home > Software engineering >  Is there a way to please the type checker other than isinstance?
Is there a way to please the type checker other than isinstance?

Time:01-11

How can I check which subclass an instance belongs to, without using isinstance?
(My understanding is that using isinstance is considered bad practice?...)
For example:

from typing import Protocol

class Entry(Protocol):
    name: str
    
    def is_file(self) -> bool: ...
    def is_folder(self) -> bool: ...

class File(Entry):
    name: str
    content: str

    def is_file(self) -> bool:
        return True
    def is_folder(self) -> bool:
        return False
    def get_size(self) -> int:
        return len(self.content)

class Folder(Entry):
    name: str
    children: list[Entry]
    
    def is_file(self) -> bool:
        return False
    def is_folder(self) -> bool:
        return True

def do_something(entry: Entry) -> None:
    if entry.is_file():
        print(entry.get_size())

On the last line of do_something the type checker understandably complains that entry has no method named get_size, because it obviously doesn't understand that I use is_file to to ensure that. (My type checker is Pylance in VSCode)
What can I do instead?
(I know this code works fine, but I would also like for it to pass type checking)

CodePudding user response:

There's no reason not to use isinstance here. It reduces the amount of unnecessary boilerplate. It isn't isinstance that is frowned up, but unnecessary run-time typechecking of any kind, including your custom is_* methods. Assuming there is a good reason for making do_something so unnecessarily generic, just use the following.

from typing import Protocol

class Entry:
    def __init__(self, *, name: str, **kwargs):
        super().__init__(**kwargs)
        self.name = name
    

class File(Entry):    
    def __init__(self, *, content: str, **kwargs):
        super().__init__(**kwargs)
        self.content = content

    def get_size(self) -> int:
        return len(self.content)


class Folder(Entry):    
    def __init__(self, *, children: list[Entry], **kwargs):
        super().__init__(**kwargs)
        self.children = children
    

def do_something(entry: Entry) -> None:
    if isinstance(entry, File):
        print(entry.get_size())

However, if do_something isn't supposed to work on anything except File objects, then say so in the type so you don't need any such type narrowing:

def do_something(entry: File) -> None:
    print(entry.get_size())

CodePudding user response:

A possibly more Pythonic way to handle this is to get rid of the is_file and is_folder completely. They're nothing but isinstance by another name.

class Entry:
    def get_size(self) -> int:
        raise NotImplementedError

class File(Entry):
    ...
    def get_size(self) -> int:
        return len(self.content)

class Folder(Entry):
    ...


def do_something(entry: Entry) -> None:
    try:
        print(entry.get_size())
    except NotImplementedError:
        pass
  • Related