cswork / libraryOpen practice workspace

Articles

Pydantic Interview Questions: Strict Validation, Optional Fields, and Validator Order

Practice Pydantic questions with tested examples of strict Python versus JSON inputs, required nullable fields, defaults, and validator execution order.

A Pydantic interview question often turns on a small distinction: a field allows None but is still required, a strict date accepts a JSON string but rejects a Python string, or a wrapper surrounds fewer validators than you expected. Guessing from the annotation alone can miss the actual input contract.

The preparation goal is to predict acceptance, output, and execution order separately. This article uses original examples checked locally with Pydantic 2.13.5. Official documentation supplies the behavior definitions; the test matrix and debugging exercises are our own. They are not claims about a particular employer’s interview questions.

For related reasoning practice, start with Reason About Nested JSON Schema Validation. Explain which inputs are valid before implementing the validator.

What should you predict before running a model?

Write down four things: the input representation, the model declaration, the validation entry point, and the expected result. “This is a date string” is insufficient if one call receives raw JSON and another receives an already decoded Python object. Similarly, “the field is optional” does not say whether omission is permitted or only whether None is a valid value.

For an original debugging exercise, imagine an import contract with a quantity, a note, and a score. The quantity should follow an explicit coercion policy. The note may be null, but the product must decide whether callers can omit it. The score is normalized and transformed by several validators. Each choice needs its own test; a successful constructor call does not establish all three contracts.

We executed 16 checks across the examples below. The assertions compare values, selected error types, field-presence information, and a validator trace. They do not test a web framework, database, or deployment. Pinning the library version makes these observed results reproducible without implying that every older or future version behaves identically.

Does strict mode reject every string?

Official behavior: Pydantic’s strict-mode documentation distinguishes Python validation from JSON validation. Strictness reduces coercion, but JSON representations of some types, including dates, remain supported. The relevant contract includes both the type and the entry point.

Here is a compact set of calls from the exercise:

from datetime import date
from pydantic import TypeAdapter

integer = TypeAdapter(int)
integer.validate_python("7")
integer.validate_python("7", strict=True)

calendar_date = TypeAdapter(date)
calendar_date.validate_python("2026-09-10", strict=True)
calendar_date.validate_json('"2026-09-10"', strict=True)

These calls are independent cases; the failing calls must be caught when running them in a single script. The local results were:

CaseObserved result
Integer string, lax Python inputInteger 7
Integer string, strict Python inputint_type error
Date string, strict Python inputdate_type error
Date string, strict JSON inputdate(2026, 9, 10)

The JSON call receives a JSON-encoded string, including its quotation marks. Passing an unquoted date token would test malformed JSON instead. Decoding that same JSON first and calling Python validation also changes the validation path. These are useful counterexamples to the claim that strict mode simply means “never accept strings.”

In a practical answer, specify where coercion belongs. A human-entered configuration may intentionally normalize textual numbers. A typed internal message may reject them to expose an upstream defect. Strictness is a contract choice, not a universal measure of code quality.

Why can a nullable field still be missing?

Official V2 rule: The migration guide explains that an annotation allowing None does not create a default. A nullable field without a default remains required.

from pydantic import BaseModel

class RequiredNote(BaseModel):
    note: str | None

class OmittableNote(BaseModel):
    note: str | None = None

In the local exercise, omitting note from RequiredNote produced a missing error. Supplying note=None succeeded. Omitting it from OmittableNote also succeeded and produced None.

That gives you two independent questions to ask about a field: must the caller supply it, and may its value be null? Avoid repairing a missing-field failure by automatically adding = None. If the intended contract requires an explicit choice, that edit weakens validation rather than fixing it.

Consider an original update operation where omission means “leave the note unchanged” and explicit null means “clear it.” Both constructions of OmittableNote have the same attribute value, but their presence information differs. Our check found an empty model_fields_set for omission and {"note"} for explicit null. An application can use that distinction when implementing its update contract.

The model does not decide those update semantics for you. A persistence layer that blindly writes every attribute could still erase the old note. Explain how the validated object will be consumed, and add an application-level check for the intended update behavior before claiming the whole feature is correct.

Can a default violate the annotation?

Official behavior: The fields documentation states that defaults are not validated by default. The validate_default setting enables that check.

from pydantic import BaseModel, Field

class Unchecked(BaseModel):
    attempts: int = "oops"

class Checked(BaseModel):
    attempts: int = Field(
        default="oops", validate_default=True
    )

The first model retained the string "oops" when constructed without an argument in our test. The second failed with int_parsing. This is a deliberately bad default, not a recommended declaration. It demonstrates why inspecting supplied inputs alone can leave a hole in the contract.

A meaningful repair starts with the intended value. If the default should be an integer, supply a valid integer. If it is derived from another source, decide whether validation is needed on that path too. Turning on default validation is useful evidence, but it does not justify an arbitrary fallback when the source is invalid.

During review, distinguish constructor defaults from caller-supplied values. Test both paths. A model that rejects attempts="oops" when explicitly supplied can still contain that value through an unchecked default, so a single invalid-input test is not enough.

Trace validator order instead of memorizing a slogan

Official ordering rule: In the validator documentation, before and wrap validators enter from right to left in annotated metadata, while after validators run from left to right. Decorator validators are incorporated after the existing field metadata. A wrapper’s placement determines what its handler surrounds.

This original example deliberately interleaves the types:

from typing import Annotated
from pydantic import (
    AfterValidator, BeforeValidator, WrapValidator,
    BaseModel, field_validator,
)

log = []

def before(value):
    log.append("before")
    return value.strip()

def after1(value):
    log.append("after1")
    return value + 1

def after2(value):
    log.append("after2")
    return value * 2

def wrap(value, handler):
    log.append("wrap-enter")
    result = handler(value)
    log.append("wrap-exit")
    return result

Score = Annotated[
    int,
    BeforeValidator(before),
    AfterValidator(after1),
    WrapValidator(wrap),
    AfterValidator(after2),
]

class Scored(BaseModel):
    score: Score

    @field_validator("score", mode="after")
    @classmethod
    def final(cls, value):
        log.append("decorator-after")
        return value

result = Scored(score=" 7 ")

The observed value was 16. The trace was wrap-enter, before, after1, wrap-exit, after2, then decorator-after. Inside the handler, whitespace was removed, the integer was validated, and one was added. After the wrapper returned, the second after-validator doubled eight to sixteen.

The wrapper did not surround every validator in the declaration. This is why predicting only “wrap runs first” is incomplete. Say where control enters, what handler executes, and which transformations remain after it returns.

What assumptions are hidden in that validator?

The before function intentionally assumes a string and calls .strip(). That is acceptable for demonstrating this one input path, but it is not a robust general integer-field validator. A caller supplying an integer would not support that operation. The next repair should define the accepted raw inputs, then guard or reject other types deliberately.

Likewise, the global log is instrumentation for a single-threaded exercise. Clear it between independent cases. It is not a production request log, and sharing it across concurrent validations would mix evidence from unrelated calls. Keeping instrumentation separate from business behavior makes the trace easier to trust.

Our test asserts both the final value and the exact trace. Either one alone could miss a regression. Two transformations might coincidentally produce the same value for a special input, while a changed order could matter for another. Choose an input such as seven that makes addition and multiplication visibly noncommutative.

As a follow-up, move the wrapper and predict the trace before rerunning. Do not silently change the expected assertion until the test passes. Explain why the new placement changes the scope of the handler and whether that is the behavior the contract needs.

Can a before-validator change a strict result?

In another original check, a strict model with an integer field rejected "7" with int_type. Adding a before-validator that explicitly converted strings with int(value) made that particular input succeed with integer seven.

This is not strict mode failing. The custom preprocessing changed what the built-in integer validator received. The important review question is whether that conversion was intended. If the requirement is to reject textual quantities at the boundary, adding such normalization defeats the requirement even though the final attribute is an integer.

Keep conversion errors, validation failures, and programming defects distinct when discussing a repair. A normalization function should have a defined input domain and failure behavior. Avoid catching every exception and returning zero: that would make malformed input indistinguishable from a legitimate quantity of zero and remove evidence of the upstream problem.

Are mutable defaults always shared?

Do not transfer the ordinary Python function-default rule to every Pydantic model without checking it. Officially, the fields guide describes copying unhashable defaults for model instances. Our local list-default check created two models, appended to the first list, and observed that the second remained empty.

That result applies to the tested list case; it is not a promise about every custom object. A default factory can still make creation intent clearer. In an interview, explain the observed behavior and why you would choose an explicit factory, rather than claiming a shared-list bug that the example does not have.

When reporting a failure, include the field location and error category that explain the contract violation. Avoid snapshotting every character of a formatted exception unless exact wording is part of your compatibility requirement. A focused assertion is easier to interpret when the library changes.

Five exercises for stronger validation reasoning

Practice questionWhat to explain
Debug Validation Error AggregationWhich failure evidence should survive
Reason About Nested JSON Schema ValidationRequired fields versus allowed values
Validate Nested Configuration ObjectsInput shape and error location
Implement retry wrapper and interdependent validatorsExecution order and dependency assumptions
Design a JSON Schema ValidatorA precise acceptance contract

Return to the nested-schema exercise with three columns in your notes: input, expected result, and reason. Then add the smallest executable check that could disprove your prediction. That habit produces clearer answers than memorizing a list of decorators without tracing their behavior.

Sources and Further Reading