SQLAlchemy returns `list` instead of `Sequence[Row[_TP]]`

Question:

The type-hinting of the method sqlalchemy.engine.result.Result.fetchall() suggests the method is supposed to return a Sequence[Row[_TP]] but when I print print(type(result_1.fetchall())), I get a list.
enter image description here

Is there anything I’m missing?

My code is the following:

if __name__ == "__main__":
    engine: sqlalchemy.engine.base.Engine = sqlalchemy.create_engine(
        "sqlite:///data_SQL/new_db.db", echo=False
    )

    with engine.connect() as connection:
        result_1: sqlalchemy.engine.cursor.CursorResult = connection.execute(
            sqlalchemy.text("SELECT * FROM MY_TABLE WHERE batch = 'sdfg';")
        )
        data_fetched_1: sqlalchemy.Sequence = result_1.fetchall()
        print(type(data_fetched_1))

Answers:

Taking a look at the source code,

    def fetchall(self) -> Sequence[Row[_TP]]:
        """A synonym for the :meth:`_engine.Result.all` method."""

        return self._allrows()

fetchall just returns self._allrows()

And this is the function signature of self._allrows:

(method) def _allrows() -> List[Row[_TP@Result]]

So that is why you’re getting a type list

Answered By: sergenp

list is a subtype of the abstract base class Sequence. You cannot annotate a variable with list and then assign a Sequence to it because that Sequence could be something other than a list, such as a tuple for example, which happens to also be a subtype of Sequence.

from collections.abc import Sequence


print(issubclass(list, Sequence))  # True
print(issubclass(tuple, Sequence))  # True

In general, if you expect your variable to behave like any sequence, i.e. support subscripting (via __getitem__) and all the other methods expected of the sequence protocol, you should annotate it as such, meaning with collections.abc.Sequence. But then you cannot expect it to support e.g. list-specific methods like append.

If you want to make sure you are dealing with a list, every Sequence can be turned into a list by simply calling the list constructor on it (because Sequence is a subtype of Iterable).

More to the point, I am not sure where you are getting your type annotations for SQLAlchemy from, but they are likely outdated. My PyCharm shows the signature of Result.fetchall to return list[Row] and so do the latest official sqlalchemy-stubs. (Same for Result.all by the way.) So a variable of type list should definitely accept the output of that method and your type checker is just using wrong type stubs.

The good news is that when SQLAlchemy 2 finally drops, we will no longer have these issues because it will thankfully feature inline type annotations.

PS

Oops, totally missed that version 2 has been out for a while now. Thanks to @sergenp for pointing it out. The current annotations indeed show Sequence[Row[_TP]].

Since the Result class is specified to inherit from ResultInternal[Row[_TP]], which is parameterized by _R and its _allrows method returns a list[_R], the annotations are still technically correct. The fetchall method returns the output of the _allrows method. list[Row[_TP]] is a subtype of Sequence[Row[_TP]].

It is best practice to declare the return type as narrow as possible, but in this case the developers did not. I can only speculate that they might plan to have fetchall return some other sequence object that is not a list in the future and to ensure stability of the API they annotated it with the broader type. You can always narrow the return type later maintaining backwards-compatibility, but you cannot broaden or change it to a different (not descendant) type later because that breaks backwards compatibility. But this is just guessing on my part.

Either way, as users of their API we must take their type hints at face value and expect whatever they say fetchall returns. We must not assume it returns a list, even though it does now. _allrows is a protected method and thus considered implementation detail.

The advice I gave in my original answer still holds: If you are fine with just any sequence, annotate your variable accordingly. If you need a list, convert the output of fetchall to a list explicitly.

Answered By: Daniil Fajnberg
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.