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 SoftBook
s, 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 Book
s 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, Book
s 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