Skip to content

Pydantic Models Interview Questions & Answers

13 questions Updated 2026-06-20 Share:

FastAPI Pydantic model interview questions — BaseModel, Field, model_config, nested models, inheritance, computed fields and ORM mode.

13 of 13

BaseModel is Pydantic's foundation class. Subclasses declare fields as class-level annotations; Pydantic validates and coerces incoming data at instantiation time.

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    quantity: int = 1    # default value

item = Item(name="Widget", price=9.99)
# Item(name='Widget', price=9.99, quantity=1)

Item(name="Widget", price="nine")   # raises ValidationError: price must be float

FastAPI uses BaseModel to:

  1. Parse and validate incoming JSON bodies.
  2. Serialise outgoing responses.
  3. Generate OpenAPI JSON Schema.

Rule of thumb: use a BaseModel subclass for any structured data that crosses the HTTP boundary — it's free validation and documentation.

Use Field() from Pydantic:

from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str = Field(min_length=1, max_length=100, description="Product name")
    price: float = Field(gt=0, description="Price in USD", example=9.99)
    sku: str = Field(pattern=r"^[A-Z]{3}-\d{4}$", examples=["ABC-1234"])

Field() parameters:

  • Constraints: gt, ge, lt, le, min_length, max_length, pattern, multiple_of
  • Metadata: title, description, example, examples
  • Behaviour: default, default_factory, alias, exclude

Rule of thumb: use Field() for any field that needs a constraint or a helpful description — it ends up in the OpenAPI schema and saves clients guessing.

  • Required: no default value — Pydantic raises ValidationError if omitted.
  • Optional with None: field: T | None = None — accepts None or the type.
  • Optional with default: field: T = default_value.
from pydantic import BaseModel

class User(BaseModel):
    id: int                      # required
    name: str                    # required
    bio: str | None = None       # optional, defaults to None
    role: str = "viewer"         # optional, defaults to "viewer"

In OpenAPI:

  • Required fields → required: [...] array in the schema.
  • Optional fields → absent from required, with default or nullable.

Rule of thumb: think of None default as "not provided"; use a semantic default like "viewer" when the field has a meaningful fallback value.

An alias lets the JSON key differ from the Python attribute name. Useful when the API uses camelCase, hyphens, or reserved Python keywords.

from pydantic import BaseModel, Field

class Order(BaseModel):
    order_id: int = Field(alias="orderId")        # JSON: "orderId"
    item_count: int = Field(alias="itemCount")

# Parsing from JSON (camelCase input)
order = Order.model_validate({"orderId": 42, "itemCount": 3})
print(order.order_id)  # 42   (Python snake_case attribute)

For global camelCase ↔ snake_case conversion use model_config:

from pydantic import ConfigDict

class MyModel(BaseModel):
    model_config = ConfigDict(alias_generator=lambda s: s.replace("_", ""), populate_by_name=True)
    order_id: int

Rule of thumb: use alias for a few fields; use alias_generator when the entire API uses camelCase (common with JavaScript clients).

A child model inherits all fields from its parent and can add or override them.

class ItemBase(BaseModel):
    name: str
    price: float

class ItemCreate(ItemBase):
    # used for POST body — no id yet
    pass

class ItemUpdate(ItemBase):
    # all fields optional for PATCH
    name: str | None = None
    price: float | None = None

class ItemOut(ItemBase):
    id: int            # added by DB
    created_at: datetime

This pattern keeps field definitions DRY while giving each use-case the exact shape it needs.

Rule of thumb: define a Base model with shared fields, then derive Create, Update, and Out variants — the "Input/Output DTO" pattern.

model_config = ConfigDict(...) replaces Pydantic v1's inner class Config. Key options:

Setting Effect
extra="forbid" Reject extra fields
frozen=True Make instances immutable (hashable)
populate_by_name=True Allow both alias and name
from_attributes=True Parse from ORM objects (SQLAlchemy)
str_strip_whitespace=True Auto-strip leading/trailing spaces
alias_generator=fn Auto-generate aliases for all fields
from pydantic import BaseModel, ConfigDict

class StrictUser(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    id: int
    name: str

Rule of thumb: set from_attributes=True on any model that reads from SQLAlchemy ORM objects; set extra="forbid" on request models to catch client typos.

By default Pydantic models only parse from dicts. from_attributes=True allows parsing from any object with attributes — including SQLAlchemy ORM instances.

from pydantic import BaseModel, ConfigDict
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase): pass

class UserORM(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String)

class UserOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str

user_orm = session.get(UserORM, 1)
user_out = UserOut.model_validate(user_orm)   # reads .id and .name attributes

Without from_attributes=True, model_validate(orm_obj) raises a ValidationError because ORM objects aren't dicts.

Rule of thumb: always set from_attributes=True on output schemas that will be constructed from SQLAlchemy models.

Use @computed_field (Pydantic v2):

from pydantic import BaseModel, computed_field

class Rectangle(BaseModel):
    width: float
    height: float

    @computed_field
    @property
    def area(self) -> float:
        return self.width * self.height

r = Rectangle(width=3.0, height=4.0)
print(r.area)           # 12.0
print(r.model_dump())   # {"width": 3.0, "height": 4.0, "area": 12.0}

@computed_field fields are included in serialisation and the OpenAPI schema. They are always read-only — you can't set them from input.

Rule of thumb: use @computed_field for derived values that belong in the response (full name from first + last, URL from ID); avoid heavy computation in them.

Model.model_validate(obj) is the explicit way to parse data in Pydantic v2. It accepts a dict or any object (with from_attributes=True) and returns a validated model instance.

# constructor — same as model_validate for dicts
item = Item(name="Widget", price=9.99)

# model_validate — more explicit, required for non-dict input
item = Item.model_validate({"name": "Widget", "price": 9.99})
item = Item.model_validate(orm_instance)   # needs from_attributes=True

In FastAPI, model_validate is called internally when parsing request bodies. You call it explicitly when converting ORM objects to Pydantic models in service or repository layers.

Rule of thumb: use the constructor for tests with literal dicts; use model_validate in production code where the source object might be an ORM row.

model_dump() returns a Python dict of the model's fields. It's Pydantic v2's replacement for .dict().

item = Item(name="Widget", price=9.99)
item.model_dump()
# {"name": "Widget", "price": 9.99}

# exclude fields
item.model_dump(exclude={"price"})
# {"name": "Widget"}

# only unset fields (for PATCH)
item.model_dump(exclude_unset=True)

# JSON-safe output (datetime → str, UUID → str)
item.model_dump(mode="json")

Rule of thumb: use model_dump(exclude_unset=True) in PATCH handlers to get only the fields the client explicitly sent.

Call Model.model_json_schema():

import json
from pydantic import BaseModel, Field

class Item(BaseModel):
    name: str = Field(min_length=1)
    price: float = Field(gt=0)

print(json.dumps(Item.model_json_schema(), indent=2))
# {
#   "title": "Item",
#   "type": "object",
#   "properties": {
#     "name": {"type": "string", "minLength": 1},
#     "price": {"type": "number", "exclusiveMinimum": 0}
#   },
#   "required": ["name", "price"]
# }

FastAPI embeds this schema in /openapi.json automatically. You might call model_json_schema() directly to validate schemas in tests or export them to other systems.

Rule of thumb: write a test that calls model_json_schema() and asserts key properties — it catches breaking schema changes before they hit production.

Feature Pydantic v1 Pydantic v2
Config class Config: model_config = ConfigDict(...)
ORM mode orm_mode = True from_attributes=True
Serialise .dict() / .json() .model_dump() / .model_dump_json()
Parse MyModel(**data) / .parse_obj() .model_validate(data)
Validators @validator @field_validator / @model_validator
Performance Pure Python Rust core (10-50× faster)

FastAPI 0.100+ requires Pydantic v2. Code targeting both versions uses the pydantic.v1 compatibility shim.

Rule of thumb: always use Pydantic v2 APIs in new code; if maintaining a v1 codebase, migrate validators first (they have the most breaking changes).

A discriminated union uses a Literal field as a type tag to unambiguously select which model to use during parsing, instead of trying each model in order.

from typing import Literal, Union, Annotated
from pydantic import BaseModel, Field

class Cat(BaseModel):
    type: Literal["cat"]
    meows: bool

class Dog(BaseModel):
    type: Literal["dog"]
    barks: bool

class PetPayload(BaseModel):
    pet: Annotated[Union[Cat, Dog], Field(discriminator="type")]

payload = PetPayload.model_validate({"pet": {"type": "dog", "barks": True}})
print(type(payload.pet))  # <class 'Dog'>

Discriminated unions are:

  • Faster — no trial-and-error parsing.
  • Clearer errors — "expected type to be 'cat' or 'dog'" vs generic failure.
  • Better OpenAPI — generates oneOf with a discriminator property.

Rule of thumb: whenever a body can be one of several shapes, add a type tag and use a discriminated union — it's explicit, fast, and self-documenting.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel