You are missing two things:
- You need to install typing stubs for SQLAlchemy.
- The
Query.filter()
method has a decorator that defines what is returned.
Typing stubs for SQLAlchemy
You want to install the sqlalchemy-stubs
project, it provides stubs for the SQLAlchemy API.
Note that even with this stub installed you still will see issues with Pyright (the checking tool underpinning the Pylance extension), because static stubs cannot fully represent the dynamic nature of some parts of the SQLAlchemy API, such as model column definitions (e.g. if your Person
model has a column called name
, defined with name = Column(String)
, then the stubs can't tell Pyright that name
will be a string). The sqlalchemy-stubs
project includes a plugin for the mypy type checker to handle the dynamic parts better, but such plugins can't be used with other type checkers.
With the stubs installed, Pylance can tell you about filter
:
Query.filter()
decorator details
The Query.filter()
method implementation is not actually operating on the original instance object; it has been annotated with a decorator:
@_generative(_no_statement_condition, _no_limit_offset)
def filter(self, *criterion):
...
The @_generative(...)
part is significant here; the definition of the decorator factory shows that the filter()
method is essentially replaced by this wrapper method:
def generate(fn, *args, **kw):
self = args[0]._clone()
for assertion in assertions:
assertion(self, fn.__name__)
fn(self, *args[1:], **kw)
return self
Here, fn
is the original filter()
method definition, and args[0]
is the reference to self
, the initial Query
instance. So self
is replaced by calling self._clone()
(basically, a new instance is created and the attributes copied over), it runs the declared assertions (here, _no_statement_condition
and _no_limit_offset
are such assertions), before running the original function on the clone.
So, what the filter()
function does, is alter the cloned instance in place, and so doesn't have to return anything; that's taken care of by the generate()
wrapper. It is this trick of swapping out methods with utility wrappers that confused Pyright into thinking None
is returned, but with stubs installed it knows that another Query
instance is returned instead.