Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Pydantic Default Values: Can You Tell the Difference?

Is your value missing or set to default in Pydantic? Learn how to distinguish between accepted default and unset fields using model_fields_set.
Split-screen image showing a confused Python developer looking at Pydantic model defaults on one side, and the same developer celebrating after using model_fields_set to resolve the issue. Visual representation of distinguishing between default values and explicitly set ones. Split-screen image showing a confused Python developer looking at Pydantic model defaults on one side, and the same developer celebrating after using model_fields_set to resolve the issue. Visual representation of distinguishing between default values and explicitly set ones.
  • ⚙️ pydantic model_fields_set lets 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=True in 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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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
  • name must be present. It will give an error if you leave it out.
  • nickname is optional. If you do not include it, it becomes None.
  • age defaults to 18 if 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 profile was set at the top level.
  • And in profile, only bio was 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_set for very precise input checks.
  • ✅ It's better to use exclude_unset=True when 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 about include/exclude parameters.

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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading