Home > Blockchain >  Python Type Hint for Returning Concrete Class
Python Type Hint for Returning Concrete Class

Time:01-07

I have some code (attached below) that I'm running with Python 3.10.

The code runs fine, but pylance in VS Code flags an error for these lines:

books: list[SoftBack] = [softback_book_1, softback_book_2]
processed_books = BookProcessor(books).process()

This is because the BookProcessor class type hints say that it will take a list[Book] and return a list[Book]. Whereas I'm actually giving it a list[SoftBack] and expecting it to return a list[SoftBack], as SoftBack is a concrete class of Book.

The error is:

(variable) books: list[SoftBack]
Argument of type "list[SoftBack]" cannot be assigned to parameter "books" of type "list[Book]" in function "__init__"
  "list[SoftBack]" is incompatible with "list[Book]"
    TypeVar "_T@list" is invariant
      "SoftBack" is incompatible with "Book" Pylancereport(GeneralTypeIssues)

Should I be using a different type hint for returning concrete classes, or is pylance incorrect in flagging this up? (Or am I doing Python wrong?!).

"""
Book Testing
"""

from abc import ABC, abstractmethod
from copy import deepcopy
from typing import Any

class Book(ABC):
    """
    A generic book.
    """

    name: str

    @abstractmethod
    def __init__(self, *args: Any | None, **kwargs: Any | None) -> None:
        """
        Abstract initialiser.
        """
        raise NotImplementedError

class SoftBack(Book):
    """
    A softback book.
    """

    name: str

    def __init__(self, name: str) -> None:
        self.name = name

class BookProcessor:
    """
    A simple book processor.
    """

    books: list[Book]

    def __init__(self, books: list[Book]) -> None:
        self.books = books

    def process(self) -> list[Book]:
        """
        Add the string '_processed' to book names,
        returning a new list of books.
        """
        processed_books: list[Book] = []
        for book in self.books:
            new_book = deepcopy(book)
            new_book.name  = '_processed'
            processed_books.append(new_book)
        return processed_books

def main():
    """
    Main function.
    """
    softback_book_1 = SoftBack(name='book_01')
    softback_book_2 = SoftBack(name='book_02')
    books: list[SoftBack] = [softback_book_1, softback_book_2]
    processed_books = BookProcessor(books).process()
    for processed_book in processed_books:
        print(processed_book.name)

if __name__ == '__main__':
    main()

CodePudding user response:

Make BookProcessor generic so that you can capture the exact type of Book being processed.

from typing import Generic, TypeVar


B = TypeVar('B', bound=Book)


class BookProcessor(Generic[B]):
    """
    A simple book processor.
    """

    books: list[B]

    def __init__(self, books: list[B]) -> None:
        self.books = books

    def process(self) -> list[B]:
        """
        Add the string '_processed' to book names,
        returning a new list of books.
        """
        processed_books: list[B] = []
        for book in self.books:
            new_book = deepcopy(book)
            new_book.name  = '_processed'
            processed_books.append(new_book)
        return processed_books

When you instantiate BookProcessor with a list of SoftBooks, the value of B will be "bound" to SoftBook to make the type BookProcess[SoftBook], giving you the desired return type of list[SoftBook] for process.

CodePudding user response:

Changing

books: list[SoftBack] = [softback_book_1, softback_book_2] # Passes type test.
processed_books = BookProcessor(books).process() # Type error.

to

books: list[Book] = [softback_book_1, softback_book_2] # Passes type test.
processed_books = BookProcessor(books).process() # Passes type test.

gets rid of the type error.

In your case, SoftBack books are Books because SoftBack inherits from the Book abstract base class. BookProcessor appends any book that inherits from Book. The type of processed_books is list[Book], therefore, Books need to be appended.

Both books: list[SoftBack] and books: list[Book] will work. However, if using another class not inheriting from Book as type, Pylance flags the items in the list as incompatible.

class TestBook:
    """A new book, which is not actually a Book.
    Doesn't inherit from the Book abstract base class.
    """
    pass

books: list[TestBook] = [softback_book_1, softback_book_2] # Type error
  • Related