How to infer different typehint for class and instance variables?

78 Views Asked by At

Some context

I am prototyping a custom ORM inspired by SQLModel and SQLAlchemy. I tested both and they seem to have different annotation behaviour for fields.

Let's consider the two following implementations of a User model in those two frameworks:

SQLModel:

from typing import Optional

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


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


if __name__ == '__main__':
    # Create database
    engine = create_engine("sqlite+pysqlite:///:memory:")
    SQLModel.metadata.create_all(engine)
    session = Session(engine)
    # Add some test data
    test = User(name="test")
    session.add(test)
    session.commit()
    # Test a query
    stmt = select(User).where(User.name.in_(["test"]))
    for user in session.scalars(stmt):
        print(user.name)

SQLAlchemy:

from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(30))


if __name__ == '__main__':
    # Create database
    engine = create_engine("sqlite+pysqlite:///:memory:")
    Base.metadata.create_all(engine)
    session = Session(engine)
    # Add some test data
    test = User(name="test")
    session.add(test)
    session.commit()
    # Test a query
    stmt = select(User).where(User.name.in_(["test"]))
    for user in session.scalars(stmt):
        print(user.name)

  • SQLModel infer the type properly on the instances of the class. PyCharm linter properly indicates user.name is a string. However User.name.in_ is seen as an error. This is not really practical if we need to pass filtering object to/from functions. Note that mypy fails on this implementation.
  • SQLAlchemy does the opposite: User.name.in_ is properly type checked but the user.name is not. Passing model instance attributes to function is not very handy as there is basically no type checking. Here mypy gives expected success.

Above implementations were tested with sqlmodel-0.0.14 and SQLAlchemy-2.0.26 on Python 3.11.

Just for clarification, I am not planning on using any SQLModel nor SQLAlchemy for my project. They are just a inspiration for elaborating a completely different ORM not even based on SQL databases.

Best of both worlds?

While keeping the model implementation as concise as possible, I'd like to provide a proper type checking for both class variables and instance variables.

My guess is that both SQLModel and SQLAlchemy rely on meta-classes for dealing with the field/attribute parts which makes proper type checking not possible but I might be wrong.

Considering the following (crud) base implementation, is there a way to provide type hints so both class and instance variable are properly type checked and where PyCharm's linter properly autocomplete Example.name and model_instance.name?


class Field:

    def filter(self):
        pass


class BaseModel(object):

    def __init__(self, **kwargs):
        # Crud way of setting the instance variables
        for k, v in kwargs.items():
            setattr(self, k, v)


class ExampleModel(BaseModel):
    name: str = Field()


if __name__ == '__main__':
    assert isinstance(ExampleModel.name, Field)
    model_instance = ExampleModel(name="test")
    assert isinstance(model_instance.name, str)

0

There are 0 best solutions below