pythonsqlalchemyrelationshipmypyflake8

SQLAlchemy string relationships cause 'undefined name' complaints by flake8 and mypy


# order.py
class Order(Base):
    __tablename__ = "Order"

    id: Mapped[int] = mapped_column(primary_key=True)
    items: Mapped[List["Item"]] = relationship(back_populates="order")

# item.py
class Item(Base):
    __tablename__ = "Item"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("Order.id"))
    order: Mapped["Order"] = relationship(back_populates="items")

I'm using sqlalchemy 2.0.23 and not using plugins for mypy or flake8.

Neither module should need to import the other since I'm using string relationships like "Item" in the Order class and "Order" in the relationship in the Item class.

flake8 reports an F821 error on both files because of the undefined name.
mypy reports a similar error on both.

I can configure flake8 to ignore the F821. I'm not sure how I'd do similar in mypy. But these are important rules that shouldn't be turned off to get SQLAlchemy classes through linters.

I want to keep my classes in separate files. Is there a way to correctly define them so that linters like these won't complain? Adding imports to both files quiets these linters, but results in a circular import problem so the code won't run.


Solution

  • mypy and flake8 are correct here, these warnings shouldn't be ignored. To resolve the circular import problem, you can use "typechecking-only import", i.e. an import statement wrapped in if TYPE_CHECKING block (TYPE_CHECKING constant is explained here) . Here's how it may look like:

    # order.py
    from typing import TYPE_CHECKING
    
    if TYPE_CHECKING:
        from .item import Item
    
    
    class Order(Base):
        __tablename__ = "Order"
    
        id: Mapped[int] = mapped_column(primary_key=True)
        items: Mapped[List["Item"]] = relationship(back_populates="order")
    

    and

    # item.py
    from typing import TYPE_CHECKING
    
    if TYPE_CHECKING:
        from .order import Order
    
    
    class Item(Base):
        __tablename__ = "Item"
    
        id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
        order_id: Mapped[int] = mapped_column(ForeignKey("Order.id"))
        order: Mapped["Order"] = relationship(back_populates="items")