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
