Typorithm · easy

Required, nullable and omitted are three different decisions

pythonpydanticindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Required, nullable and omitted are three different decisions

0%

Concept and contract

Preserve the difference between a missing field and an explicitly supplied null.

A field can be required while still accepting None. The annotation str | None permits two value types; it does not supply a default. In a patch request this distinction matters because the absence of a key may mean leave the stored value unchanged, while an explicit null may mean clear it.

The baseline requires nickname but permits its value to be null. bio has a default of None and can be omitted. Read those declarations separately: requirement answers whether a key must be present, nullability answers which value can be supplied, and a default answers what is used when a field is absent.

Mechanism and worked example

Pydantic records the names supplied at validation time in model_fields_set.

Pydantic records the names supplied at validation time in model_fields_set. model_dump(exclude_unset=True) uses that information when building a patch mapping. It includes an explicitly supplied nickname=None but omits bio when no bio was sent.

The example returns both the selected values and the sorted field set so the fixture can observe this policy directly. For nickname="Ada", only nickname appears. For nickname=None, that same key remains present with null. With no nickname key, the baseline returns a missing error rather than silently creating a patch.

Complete worked example

python

from pydantic import BaseModel, ValidationError

class ProfilePatch(BaseModel):
    nickname: str | None
    bio: str | None = None

def inspect_example(payload):
    try:
        patch = ProfilePatch.model_validate(payload)
        return {"values": patch.model_dump(exclude_unset=True),
                "set": sorted(patch.model_fields_set)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Pydantic records the names supplied at validation time in model_fields_set. model_dump(exclude_unset=True) uses that information when building a patch mapping. It includes an explicitly supplied nickname=None but omits bio when no bio was sent. The example returns both the selected values and the sorted field set so the fixture can observe this policy directly. For nickname="Ada", only nickname appears. For nickname=None, that same key remains present with null. With no nickname key, the baseline returns a missing error rather than silently creating a patch.

Focused decision: use the complete example for context

python

from pydantic import BaseModel

class ProfilePatch(BaseModel):
    nickname: str | None
    bio: str | None = None

def submitted_values(payload):
    patch = ProfilePatch.model_validate(payload)
    return patch.model_dump(exclude_unset=True)
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

Using exclude_none=True is a different operation.

Using exclude_none=True is a different operation. It removes values that are None even when the caller explicitly supplied them. Replacing exclude_unset with exclude_none can therefore prevent a user from clearing an optional stored value, while a careless full model_dump may instead add defaults the caller never sent.

Do not treat truthiness as field presence either: an empty string, false or zero can be explicitly supplied. Decide whether those values are valid in their own field contracts. A patch model also needs an allowlist of writable fields and object authorization; preserving presence correctly does not make every requested change permissible.

Controlled mistake: observe the actual wrong behavior

python

from pydantic import BaseModel, ValidationError

class ProfilePatch(BaseModel):
    nickname: str | None
    bio: str | None = None

def inspect_example(payload):
    try:
        patch = ProfilePatch.model_validate(payload)
        return {"values": patch.model_dump(exclude_none=True),
                "set": sorted(patch.model_fields_set)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Using exclude_none=True is a different operation. It removes values that are None even when the caller explicitly supplied them. Replacing exclude_unset with exclude_none can therefore prevent a user from clearing an optional stored value, while a careless full model_dump may instead add defaults the caller never sent. Do not treat truthiness as field presence either: an empty string, false or zero can be explicitly supplied. Decide whether those values are valid in their own field contracts. A patch model also needs an allowlist of writable fields and object authorization; preserving presence correctly does not make every requested change permissible.

Independent practice: one bounded change

Make nickname genuinely optional to submit by giving it a None default.

Make nickname genuinely optional to submit by giving it a None default. Keep exclude_unset=True and the current result shape. The new behavior must accept an empty patch and a bio-only patch without inventing a nickname update.

Predict the field set for an empty mapping before running the task. An absent defaulted nickname should not appear in the patch output. Then test an explicitly supplied null and confirm that it is still retained. This is a one-declaration change with consequences that are easier to reason about using three tiny examples.

Step 1: Default nickname to None without losing the difference between omitted and explicitly null fields. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from pydantic import BaseModel, ValidationError

class ProfilePatch(BaseModel):
    nickname: str | None
    bio: str | None = None

def inspect_example(payload):
    try:
        patch = ProfilePatch.model_validate(payload)
        return {"values": patch.model_dump(exclude_unset=True),
                "set": sorted(patch.model_fields_set)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}

# Independent task: Default nickname to None without losing the difference between omitted and explicitly null fields.
Reasoning
Default nickname to None without losing the difference between omitted and explicitly null fields.
  • Hint 1: Change the nickname declaration, not the serializer.
  • Hint 2: Use a None default and retain exclude_unset=True.

Solution, tests and trade-offs

The solution adds = None to nickname.

The solution adds = None to nickname. It leaves serialization based on field presence, so the empty mapping produces empty values and an empty field set. An explicit nickname=None remains an explicit update. The tests compare the returned mappings rather than checking only that validation succeeds.

A model default is not a database default and a patch mapping is not a complete replacement object. A service must decide how to apply allowed fields atomically and how to handle uniqueness or concurrency. Keep those decisions outside this small presence lesson, and avoid converting every omitted value into a destructive update.

Practice solution and acceptance checks

python

from pydantic import BaseModel, ValidationError

class ProfilePatch(BaseModel):
    nickname: str | None = None
    bio: str | None = None

def inspect_example(payload):
    try:
        patch = ProfilePatch.model_validate(payload)
        return {"values": patch.model_dump(exclude_unset=True),
                "set": sorted(patch.model_fields_set)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The solution adds = None to nickname. It leaves serialization based on field presence, so the empty mapping produces empty values and an empty field set. An explicit nickname=None remains an explicit update. The tests compare the returned mappings rather than checking only that validation succeeds. A model default is not a database default and a patch mapping is not a complete replacement object. A service must decide how to apply allowed fields atomically and how to handle uniqueness or concurrency. Keep those decisions outside this small presence lesson, and avoid converting every omitted value into a destructive update.

Interview reasoning and next step

Explain a three-case table in a review: omitted, present with null, and present with a value.

Explain a three-case table in a review: omitted, present with null, and present with a value. State which columns are accepted by the model and which keys the patch serializer emits. That is stronger evidence than describing a field as simply optional.

The next topic adds explicit bounds to values and collections. Presence is still independent of those checks. When a user reports that a field could not be cleared, inspect whether serialization discarded explicit null before investigating database behavior or adding more default values.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track pydantic, then edit the working exercise baseline and run --practice pyd-required. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Preserve the difference between a missing field and an explicitly supplied null.