4 min read
How to generate Pydantic models with optional fields

Pydantic is a great data-validation library that we use to define the models of the entities that live in our database. I like the fact that is as simple as declaring a type hint to get your data validated. But sometimes you’d like to have a slightly different version of your model.

The problem

Let’s start with a basic example. Suppose you have a database model like this:

from pydantic import BaseModel

class User(BaseModel):
	id: int
	first_name: str
	last_name: str

This model mirrors the structure of your database. So far, so good.

Now, say you want to create a new User. You can’t provide the id up front — it gets generated when the user is created. So, you define a new model:

class UserCreate(BaseModel):
	name: str
	last_name: str

Great, that works. But what about updating a user? You may want to change just the first_name, or just the last_name, or both. So you add:

class UserUpdate(BaseModel):
	name: str | None
	last_name: str | None

Now you’re repeating fields across multiple classes. Every time the original User model changes, you have to manually update UserCreate and UserUpdate to stay in sync. If your app has lots of models, this quickly becomes tedious—and error-prone.

A first possible solution

After browsing a bit on the internet, I found this issue in Pydantic’s github repository. It addresses exactly this pain point: how to derive a version of an existing model where all fields are optional.

Here’s the workaround suggested in the thread:

from copy import deepcopy
from typing import Any, Optional, Tuple, Type, TypeVar

from pydantic import BaseModel, create_model
from pydantic.fields import FieldInfo


class User(BaseModel):
  first_name: str
  last_name: str


def make_field_optional(field: FieldInfo, default: Any = None) -> Tuple[Any, FieldInfo]:
  new = deepcopy(field)
  new.default = default
  new.annotation = Optional[field.annotation]  # type: ignore
  return (new.annotation, new)


BaseModelT = TypeVar('BaseModelT', bound=BaseModel)

def make_partial_model(model: Type[BaseModelT]) -> Type[BaseModelT]:
  return create_model(  # type: ignore
    f'Partial{model.__name__}',
    __base__=User,
    __module__=User.__module__,
    **{
        field_name: make_field_optional(field_info)
        for field_name, field_info in User.model_fields.items()
    }
    )


PartialUser = make_partial_model(User)


print(PartialUser(first_name='Adrian'))
#> first_name='Adrian' last_name=None

The proposed solution works, but there’s a catch: you won’t get any help from type hints, because PartialUser is considered of type type[User]. For that, you’ll also get linting errors when not specifying all the parameters like: Argument missing for parameter "last_name".

A better solution

Since doing this in a dynamic way would sacrifice type hints, I came up with a different idea. What if we could generate these partial models programmatically, starting from the original model?

That’s exactly what my solution does: it’s a script that scans your Pydantic models and creates copies where all fields are preserved — including type hints — but made optional.

Key Features

  • Supports both Pydantic v1 and Pydantic v2
  • Automatically generates partial models from existing ones
  • Detects changes in model name, field names, or types to determine whether regeneration is needed
  • Use a simple @generate_partial decorator to flag models for generation
  • Add custom methods to the generated models without worrying about overwrites
  • Includes a base class for partial models where you can define shared logic or behavior

You can find the script and a minimal working example in the dedicated GitHub repository.