2

I have two Pydantic models:

from typing import List, Union
from pydantic import BaseModel

class Students:
    class Student(BaseModel):
        StudentName: str
        StudentAge: int

    class StudentRequest(BaseModel):
        Class: int
        UUID: str
        Students: Union[List[Student], None]

For the above class at Students: Union[List[Student], None], I get the error Unresolved reference 'Student'. Can we not define a model under a class and use it for segregating them?

The code below works, but I want to get an understanding whether the above BaseModel nested under a class will work or not:

class Student(BaseModel):
    StudentName: str
    StudentAge: int

class StudentRequest(BaseModel):
    Class: int
    UUID: str
    Students: Union[List[Student], None]
Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41
Lav Sharma
  • 33
  • 3
  • I don't think this is strictly related to Pydantic's BaseModel, but a general behavior with nested classes in Python. See: [Python - reference inner class from other inner class](https://stackoverflow.com/q/42185472/2745495) and [Expressing type hint of sibling nested class in class constructor](https://stackoverflow.com/q/67113582/2745495) – Gino Mempin May 25 '23 at 09:30

1 Answers1

4

You need to understand that as long as the outer class is not fully constructed (when you are still setting up things inside its namespace), you will inevitably have to deal with forward references.

So there are two mandatory things (and one optional) you need to remember, when doing this.

1) Use the qualified class name OuterClass.InnerClass

The Python interpreter itself will have no trouble with a forward reference to another inner class in an annotation. That is simply because it does not actually do anything with those annotations by default. So you could just do this:

from pydantic import BaseModel


class OuterClass:
    class Student(BaseModel):
        name: str
        age: int

    class StudentRequest(BaseModel):
        ...
        students: list["Student"]

But this will fall apart with Pydantic models because those actually use those annotations to construct objects based off of them. As you will see in the next section, at some point Pydantic will have to actually resolve the refernce to Student so get the actual underlying class at runtime. And since that will inevitably happen outside the scope of the OuterClass, without the qualified name, it will run into a NameError.

So you have to do it like this:

...

class OuterClass:
    class Student(BaseModel):
        ...

    class StudentRequest(BaseModel):
        ...
        students: list["OuterClass.Student"]

2) Update forward references after the outer class is constructed

An annotation as shown above is internally stored as a ForwardRef object.

As mentioned above, Pydantic will have to resolve those forward references eventually, for you to be able to actually use those models.

However it is not always able to do so automatically. To quote the documentation:

In some cases, a ForwardRef won't be able to be resolved during model creation. [...] When this happens, you'll need to call update_forward_refs after the model has been created before it can be used.

But with a setup like yours, when the model is nested in the namespace of an outer class, you cannot just do so after the model is created. You must do that after the outer class is created.

So with that setup, you will have to do this:

from pydantic import BaseModel


class OuterClass:
    class Student(BaseModel):
        name: str
        age: int

    class StudentRequest(BaseModel):
        ...
        students: list["OuterClass.Student"]


OuterClass.StudentRequest.update_forward_refs()

Notice that the call happens outside of OuterClass after it is created.

3) Enable postponed evaluation of annotations (optional)

Since PEP 563 you can do from __future__ import annotations at the top of your module and then omit the quotes from your forward references. This just improves readability and makes things generally easier to code.

So in total, your code should look like this:

from __future__ import annotations
from pydantic import BaseModel


class OuterClass:
    class Student(BaseModel):
        name: str
        age: int

    class StudentRequest(BaseModel):
        ...
        students: list[OuterClass.Student]


OuterClass.StudentRequest.update_forward_refs()

Demo:

print(OuterClass.StudentRequest.schema_json(indent=4))
obj = OuterClass.StudentRequest.parse_obj({
    "students": [
        {"name": "foo", "age": 18},
        {"name": "bar", "age": 19},
    ]
})
print(obj.json(indent=4))

Output:

{
    "title": "StudentRequest",
    "type": "object",
    "properties": {
        "students": {
            "title": "Students",
            "type": "array",
            "items": {
                "$ref": "#/definitions/Student"
            }
        }
    },
    "required": [
        "students"
    ],
    "definitions": {
        "Student": {
            "title": "Student",
            "type": "object",
            "properties": {
                "name": {
                    "title": "Name",
                    "type": "string"
                },
                "age": {
                    "title": "Age",
                    "type": "integer"
                }
            },
            "required": [
                "name",
                "age"
            ]
        }
    }
}
{
    "students": [
        {
            "name": "foo",
            "age": 18
        },
        {
            "name": "bar",
            "age": 19
        }
    ]
}
Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41