Search code examples
pythonsqlalchemyfastapipydanticsqlmodel

FastAPI - "TypeError: issubclass() arg 1 must be a class" with modular imports


When working with modular imports with FastAPI and SQLModel, I am getting the following error if I open /docs:

TypeError: issubclass() arg 1 must be a class

  • Python 3.10.6
  • pydantic 1.10.2
  • fastapi 0.85.2
  • sqlmodel 0.0.8
  • macOS 12.6

Here is a reproducible example.

user.py

from typing import List, TYPE_CHECKING, Optional
from sqlmodel import SQLModel, Field

if TYPE_CHECKING:
    from item import Item

class User(SQLModel):
    id: int = Field(default=None, primary_key=True)
    age: Optional[int]
    bought_items: List["Item"] = []

item.py

from sqlmodel import SQLModel, Field

class Item(SQLModel):
    id: int = Field(default=None, primary_key=True)
    price: float
    name: str

main.py

from fastapi import FastAPI

from user import User

app = FastAPI()

@app.get("/", response_model=User)
def main():
    return {"message": "working just fine"}

I followed along the tutorial from sqlmodel https://sqlmodel.tiangolo.com/tutorial/code-structure/#make-circular-imports-work. If I would put the models in the same file, it all works fine. As my actual models are quite complex, I need to rely on the modular imports though.

Traceback:

Traceback (most recent call last):
  File "/Users/felix/opt/anaconda3/envs/fastapi_test/lib/python3.10/site-packages/fastapi/utils.py", line 45, in get_model_definitions
    m_schema, m_definitions, m_nested_models = model_process_schema(
  File "pydantic/schema.py", line 580, in pydantic.schema.model_process_schema
  File "pydantic/schema.py", line 621, in pydantic.schema.model_type_schema
  File "pydantic/schema.py", line 254, in pydantic.schema.field_schema
  File "pydantic/schema.py", line 461, in pydantic.schema.field_type_schema
  File "pydantic/schema.py", line 847, in pydantic.schema.field_singleton_schema
  File "pydantic/schema.py", line 698, in pydantic.schema.field_singleton_sub_fields_schema
  File "pydantic/schema.py", line 526, in pydantic.schema.field_type_schema
  File "pydantic/schema.py", line 921, in pydantic.schema.field_singleton_schema
  File "/Users/felix/opt/anaconda3/envs/fastapi_test/lib/python3.10/abc.py", line 123, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
TypeError: issubclass() arg 1 must be a class

Solution

  • TL;DR

    You need to call User.update_forward_refs(Item=Item) before the OpenAPI setup.


    Explanation

    So, this is actually quite a bit trickier and I am not quite sure yet, why this is not mentioned in the docs. Maybe I am missing something. Anyway...

    If you follow the traceback, you'll see that the error occurs because in line 921 of pydantic.schema in the field_singleton_schema function a check is performed to see if issubclass(field_type, BaseModel) and at that point field_type is not in fact a type instance.

    A bit of debugging reveals that this occurs, when the schema for the User model is being generated and the bought_items field is being processed. At that point the annotation is processed and the type argument for List is still a forward reference to Item. Meaning it is not the actual Item class itself. And that is what is passed to issubclass and causes the error.

    This is a fairly common problem, when dealing with recursive or circular relationships between Pydantic models, which is why they were so kind to provide a special method just for that. It is explained in the Postponed annotations section of the documentation. The method is update_forward_refs and as the name suggests, it is there to resolve forward references.

    What is tricky in this case, is that you need to provide it with an updated namespace to resolve the Item reference. To do that you need to actually have the real Item class in scope because that is what needs to be in that namespace. Where you do it does not really matter. You could for example import User model into your item module and call it there (obviously below the definition of Item):

    from sqlmodel import SQLModel, Field
    
    from .user import User
    
    class Item(SQLModel):
        id: int = Field(default=None, primary_key=True)
        price: float
        name: str
    
    User.update_forward_refs(Item=Item)
    

    But that call needs to happen before an attempt is made to set up that schema. Thus you'll at least need to import the item module in your main module:

    from fastapi import FastAPI
    
    from .user import User
    from . import item
    
    api = FastAPI()
    
    @api.get("/", response_model=User)
    def main():
        return {"message": "working just fine"}
    

    At that point it is probably simpler to have a sub-package with just the model modules and import all of them in the __init__.py of that sub-package.

    The reason I gave the example of putting the User.update_forward_refs call in below your Item definition is that these situations typically occur, when you actually have a circular relationship, i.e. if your Item class had a users field for example, which was typed as list[User]. Then you'd have to import User there anyway and might as well just update the references there.

    In your specific example, you don't actually have any circular dependencies, so there is strictly speaking no need for the TYPE_CHECKING escape. You can simply do from .item import Item inside user.py and put the actual class in your annotation as bought_items: list[Item]. But I assume you simplified the actual use case and simply forgot to include the circular dependency.


    Maybe I am missing something and someone else here can find a way to call update_forward_refs without the need to provide Item explicitly, but this way should definitely work.