sqlmodel icon indicating copy to clipboard operation
sqlmodel copied to clipboard

Mypy errors: Incompatible type for optional ids & Incompatible type for SQLModel attribute in expression

Open javivdm opened this issue 2 years ago • 9 comments

First Check

  • [X] I added a very descriptive title to this issue.
  • [X] I used the GitHub search to find a similar issue and didn't find it.
  • [X] I searched the SQLModel documentation, with the integrated search.
  • [X] I already searched in Google "How to X in SQLModel" and didn't find any information.
  • [X] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [X] I already checked if it is not related to SQLModel but to Pydantic.
  • [X] I already checked if it is not related to SQLModel but to SQLAlchemy.

Commit to Help

  • [X] I commit to help with one of those options 👆

Example Code

from typing import Optional

from sqlmodel import SQLModel, create_engine, Session, Field, select


class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str


engine = create_engine("sqlite:///.deleted/database.db")

SQLModel.metadata.create_all(engine)

hero_1 = Hero(name="Grey Hound")


def format_id(id: int):
    return f"{id:04d}"


with Session(engine) as session:
    session.add(hero_1)
    session.commit()
    session.refresh(hero_1)

    session.exec(select(Hero).where(Hero.id.in_([1, 2])))
    print(format_id(hero_1.id))

Description

When executing mypy on the code above I get the following output:

sqlmodel_mypy_issue.py:27: error: Item "int" of "Optional[int]" has no attribute "in_"
sqlmodel_mypy_issue.py:27: error: Item "None" of "Optional[int]" has no attribute "in_"
sqlmodel_mypy_issue.py:28: error: Argument 1 to "format_id" has incompatible type "Optional[int]"; expected "int"
Found 3 errors in 1 file (checked 1 source file)

So:

  • Is there any way to tell mypy that upon read a SQLmodel id has type int instead of Optional[int] without having to convert it to another model?
  • How should I build expressions such as .where(Hero.id.in_([1, 2])) so that mypy and Pycharm don't flag the in_ as not being an attribute of the model attribute type?

Thank you!

Operating System

Linux

Operating System Details

No response

SQLModel Version

0.0.6

Python Version

Python 3.9.10

Additional Context

mypy version: mypy 0.931

javivdm avatar Mar 10 '22 11:03 javivdm

This might be considered an "exotic pattern", just like my #271 . :slightly_smiling_face:

StefanBrand avatar Mar 15 '22 15:03 StefanBrand

@StefanBrand note that there are two different mypy issues above.

One is the use of .in_, which honestly I think is one of the most common expressions used for building queries so I would be surprised that it would follow under the exotic patterns, but who knows.

The other is the most basic use of SQLModel which is to define a model with an id field as Optional[int] = Field(default=None, primary_key=True). That one also gets an error by mypy when trying to access the id in a function that reads the data from the db (hence the id is always filled), but mypy complains that it might be optional. There is no way it can be considered an exotic pattern, as it is the most basic functionality SQLModel provides.

javivdm avatar Mar 16 '22 18:03 javivdm

The other is the most basic use of SQLModel which is to define a model with an id field as Optional[int] = Field(default=None, primary_key=True).

Is a bit strange though to declare a PK 'optional', as it is always there, in the db model. I guess you get this though when don't have separate models for handling POST requests for creation or whatever, but just reuse the same classes. I've been using separate create, db and view classes, like in the tutorial, and find it quite nice. Anyhow am not arguing, I guess this would be nice to solve somehow.

antont avatar Mar 16 '22 18:03 antont

@antont this happens even when you use different models for db and creation. If you separate them, you can exclude the id field from the creation model, but when you create the db instance from the creation instance, the id is empty. Therefore as I understand it, if you don't have an optional int as the id field the Pydantic validation for the model will fail, as before saving it to the database the instance of the db model has no id yet. And actually in the tutorial, in the section about having different models the id is still optional, as unfortunately it has to be.

javivdm avatar Mar 17 '22 13:03 javivdm

@javivdm I autogenerate the id's at creation, so they won't be empty even though the *Create model that gets the POST data does not have them.

However my current code does declare them optional in the DB class too, I had forgotten why, and had added a comment, I can try at some point if it works when that is not Optional.

id: Optional[int] = Field(primary_key=True, nullable=False) #XXX why optional here?

In the POST handler for creation I do

dbob = dbmodel.from_orm(createob)

antont avatar Mar 17 '22 14:03 antont

This is the tutorial section @javivdm is referring to: https://sqlmodel.tiangolo.com/tutorial/fastapi/multiple-models/#multiple-hero-schemas It explains why you need Optional for the database model, and can omit it for the create model.

StefanBrand avatar Mar 17 '22 15:03 StefanBrand

@StefanBrand - right. The explanation is not very elaborate, though, but I guess it is because the db model is first instantiated in memory, with no value for id yet, and only when it's then written to the db, the id gets generated. Am still curious to try it out but I figure will get an error because of that.

antont avatar Mar 17 '22 15:03 antont

I have similar errors for a not null check in a query like MyModel.bool_field.isnot(None)

repository.py:142: error: Item "bool" of "Optional[bool]" has no attribute "isnot"  [union-attr]
repository.py:142: error: Item "None" of "Optional[bool]" has no attribute "isnot"  [union-attr]

(update: seems like this is https://github.com/tiangolo/sqlmodel/issues/109)

anentropic avatar May 18 '23 13:05 anentropic

I'm not sure about solving the first point, but to

How should I build expressions such as .where(Hero.id.in_([1, 2])) so that mypy and Pycharm don't flag the in_ as not being an attribute of the model attribute type?

Replace Hero.id.in_([1,2]) with col(Hero.id).in_([1,2]), which should make type checkers realize that you're applying in_(...) to a column object rather than directly to the type of the column.

danielunderwood avatar Aug 17 '23 21:08 danielunderwood