- ⚙️
pydantic model_fields_setlets developers distinguish between explicitly set values and defaults. - 🚫 Relying solely on
.dict()can cause accidental data overwrites in PATCH requests. - 🧠 Nested Pydantic models keep
model_fields_set. This helps you see exactly what data was put in. - 🛠️ Using
exclude_unset=Truein FastAPI relies on Pydantic's internal tracking of field assignment. - 🔍 Knowing which fields were explicitly set improves validation, auditing, and conditional logic.
More to Defaults Than You Think
When you work with Pydantic models, it's easy to think a field is just either set or not. But there's a key detail. Is a user giving a value, or did the default just get used? If you build APIs, update parts of data, or save data, knowing this difference is important. pydantic model_fields_set helps you see if a user gave a value or if the default was used. This makes your app's logic more precise.
When Defaults Matter: Real-World Scenarios
Knowing the difference between default fields and user-entered ones is not just an idea. It changes how your app works. Here are some real examples of why this matters.
🌐 PATCH APIs
In RESTful API design, a PATCH request usually means: "Only update the fields I give you." Think about an endpoint to update a user's profile. If Pydantic fills in missing fields with default values, and you save them without checking, you could accidentally replace existing data with those defaults. The client never meant to change it.
For example, look at this incoming JSON:
{
"nickname": "Sam"
}
If your backend sees missing fields like age or bio and thinks "use defaults," it might accidentally clear or reset data. And that's dangerous.
🔍 Custom Validation Rules
You might want stricter checks when a value is clearly set, even if it's the same as the default. For example, you might want to check that an email address a user typed in is real and can be reached. You would not want to rely on a placeholder email. But if the backend cannot tell if a user meant to set a value or if it was just the default, then checks might not work right. Also, they might miss things.
🗃️ Database Rewrites
If you just write default values back to the database, it can cause extra writes. It might even replace important settings. This makes pydantic detect missing fields important. This is especially true for apps that work with databases or ORMs.
Recap: How Pydantic Handles Default Values
To understand how Pydantic handles defaults, you need to know how it works with data fields.
from typing import Optional
from pydantic import BaseModel
class User(BaseModel):
name: str # Required
nickname: Optional[str] # Optional, defaults to None
age: int = 18 # Default provided
namemust be present. It will give an error if you leave it out.nicknameis optional. If you do not include it, it becomesNone.agedefaults to18if not given.
But what happens when someone sends {"age": 18} on purpose? The model will see age as 18, no matter if it was set on purpose or just used the default. But model_fields_set keeps track of the difference.
Enter model_fields_set: The Key to Knowing
Pydantic v2 brought in model_fields_set (v1 used __fields_set__). This lets you see exactly which fields the user gave.
from pydantic import BaseModel
class User(BaseModel):
name: str = "Anonymous"
age: int = 18
user = User(age=18)
print(user.model_fields_set) # Output: {'age'}
Here, both name and age have defaults. But only age shows up in model_fields_set. This shows the user set it on purpose. It's a simple and strong way to know what the user wanted.
Use Case Deep Dive: PATCH Requests and Partial Updates
Let's look again at a PATCH endpoint:
// Client payload
{
"username": "Anonymous"
}
// Or an empty payload
{}
If you use FastAPI or another framework, Pydantic will fill in any default values for the model. But only in the first example did the client actually give a username.
With model_fields_set, you can choose if you should write to the database:
if "username" in user.model_fields_set:
update_db_field("username", user.username)
This helps a lot in systems where you want to avoid writing the same data more than once. For example, it helps when starting other tasks or keeping things running fast.
Nested Models and Default Discrepancy
Does model_fields_set work well with deeply nested structures? Yes, it does.
from pydantic import BaseModel
class Profile(BaseModel):
bio: str = "Hello"
location: str = "Unknown"
class User(BaseModel):
profile: Profile = Profile()
u = User(profile=Profile(bio="Engineer"))
print(u.model_fields_set) # {'profile'}
print(u.profile.model_fields_set) # {'bio'}
This tells us:
- Only
profilewas set at the top level. - And in
profile, onlybiowas changed on purpose.
Without this info, we would not know if these values were set on purpose or if they were just defaults. This way of doing things is very helpful in systems where users can set their own choices or where settings are built in parts.
Runtime Defaults: An Easy Developer Mistake
It's easy to make a mistake when you call .dict() or .json() on a model. By default, Pydantic turns all fields into text, even ones the user never set on purpose.
user.dict() # Includes everything
user.dict(exclude_unset=True) # Only fields the user set
Using exclude_unset=True stops you from accidentally turning default fields into text.
This small change greatly improves how PATCH APIs, data converters, and UI tools work. This is because they will not wrongly see default values as something the user meant to set.
Distinguishing Between “Set to Default” and “Accepted Default”
Let's make this practical with a small tool:
def was_explicitly_set(model, field_name):
return field_name in model.model_fields_set
This works well inside:
- Pre-save actions
- Custom checks
- Audit tools
- Business logic paths
This lets you adjust how your app works based on if a user's input changed the final value.
Better Custom Checks: Knowing the Full Picture
Imagine we only want to allow certain values if users set them on purpose. One way is to pass model_fields_set into your separate checking functions.
Built-in checkers do not have direct access to the model itself (and so not to model_fields_set). But you can find a way around this:
from pydantic import BaseModel, root_validator
class Config(BaseModel):
level: str = "basic"
@root_validator
def validate_explicit_level(cls, values):
if "level" in values.get("__fields_set__", set()) and values["level"] == "basic":
raise ValueError("Users can't explicitly set 'basic'; it's the default")
return values
Or check manually after building the model with:
if model.model_fields_set and "level" in model.model_fields_set:
if model.level == "basic":
raise ValueError("Explicit setting of 'basic' not allowed.")
This way gives you full control to make your own rules for your system.
How Pydantic Changed: A Look at v2+
From Pydantic v1 to v2, the biggest change is how easy it is to read and understand the design.
- ✅ v1:
model.__fields_set__→ awkward, dunder-heavy - ✅ v2:
model.model_fields_set→ easy to understand and find
From the Pydantic v2 docs:
“BaseModel keeps track of which fields were explicitly set by the user… These can be accessed via
model_fields_set.”
This change shows a larger move toward looking inside objects more clearly and working better with other things. This is especially true as libraries like FastAPI use these features without you seeing them.
Example: How FastAPI Handles PATCH
FastAPI works very closely with Pydantic's model structures. For partial updates (PATCH), FastAPI suggests you use:
@app.patch("/users/{user_id}")
def update_user(user_id: int, user_update: User):
partial_data = user_update.dict(exclude_unset=True)
db.update(user_id, partial_data)
Why does this work as expected?
FastAPI knows to leave out fields the client did not send. This is thanks to Pydantic's own model_fields_set.
This stops:
- Accidental field resets
- Writing to the database more than once
- Turning on other services by mistake
This is a great example of how clever defaults and looking into objects can cut down on repetitive code. It also keeps your data correct.
Best Practices for Handling Optional and Default Fields
Handling defaults in BaseModel has many small points. Always do these things:
- ✅ Use
model_fields_setfor very precise input checks. - ✅ It's better to use
exclude_unset=Truewhen you change models to dict/json for PATCH APIs. - ❌ Do not assume a user gave a field just because it has a value.
- ⚠️ Do not use
.dict()or.json()without knowing aboutinclude/excludeparameters.
These steps help stop logic errors. They also make your app's data changes more expected.
Common Questions and Problems
Here are common problems. And this is how knowing about pydantic detect missing fields helps you avoid them:
Why model.dict() might mislead you
It shows all fields, even ones not set by the user. This makes it look like the user sent everything. Use exclude_unset=True to get only the values the user gave.
exclude_defaults vs. exclude_unset
exclude_unset=True: Hides fields the user did not pass.exclude_defaults=True: Leaves out fields that are the same as their default values.
Use them right, depending on if you want to limit output based on what the user did or if values are repeated.
How should I treat None?
Many APIs let you use values like null. You should treat this as a purposeful change. It is not the same as leaving something out. Use model_fields_set to see this small difference.
Making Better Data Tools with This Knowledge
The strength of pydantic model_fields_set goes far past PATCH requests:
- 💼 Audit trails: Keep track of only the fields users changed.
- 🧪 Feature toggles: Find out when new settings are actually tested.
- 🛠️ Form builders: Fill forms again using only the values users changed.
- 🧾 Diffs for caching: Send only changed settings to other systems.
Simply put, it makes your software respect what users want more. And it makes it work better.
Getting Good at Pydantic Field Details
The more you learn Pydantic, the more you see its value for careful thinking. Features like pydantic default values, pydantic model_fields_set, and pydantic detect missing fields open up new ways of programming. They help create flexible code that understands user intent and cares about specific values. With these tools, developers can build smarter APIs. They can check things with more accuracy. And they can avoid small bugs that happen when defaults are not seen. Know the difference. Then let your app logic show what was truly meant for each field.
Citations
- Pydantic. (2023). Tracking field input with model_fields_set. https://docs.pydantic.dev
- tiangolo. (2021). FastAPI GitHub Discussions on PATCH behavior. https://github.com/tiangolo/fastapi/discussions
- Python Software Foundation. (2020). Comparison of dataclass and Pydantic behavior. https://docs.python.org/3/library/dataclasses.html