<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gagan Saravanakannan</title>
    <description>The latest articles on DEV Community by Gagan Saravanakannan (@gagan-saravana).</description>
    <link>https://dev.to/gagan-saravana</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4189706%2Fd6702fbc-acad-4d57-b9de-b2b65333d2d3.png</url>
      <title>DEV Community: Gagan Saravanakannan</title>
      <link>https://dev.to/gagan-saravana</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gagan-saravana"/>
    <language>en</language>
    <item>
      <title>Pydantic Validation 101</title>
      <dc:creator>Gagan Saravanakannan</dc:creator>
      <pubDate>Sat, 17 Oct 2026 02:42:22 +0000</pubDate>
      <link>https://dev.to/gagan-saravana/pydantic-validation-101-lnl</link>
      <guid>https://dev.to/gagan-saravana/pydantic-validation-101-lnl</guid>
      <description>&lt;p&gt;Pydantic is a Python library that checks and cleans your data before your code uses it. This guide covers its 5 main ideas in plain words. Instead of memorizing syntax, the goal is to understand what happens to your data, why Pydantic works this way, and where each feature fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick picture:&lt;/strong&gt; think of Pydantic as a security guard at the door of your program. Data comes in messy, the guard checks it, fixes small problems, and either lets it in clean or turns it away with a clear note explaining why.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Models - The Door Where Data Gets Checked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Simple Idea&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Python doesn't check types when your code runs. If you pass a function a number, a string, or &lt;code&gt;None&lt;/code&gt;, it just accepts it. That's a problem because real data (web requests, database rows, environment variables) usually arrives messy and unchecked.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Pydantic Model&lt;/strong&gt; is a checkpoint for that data. You write a class that inherits from &lt;code&gt;BaseModel&lt;/code&gt; and list the fields with their types. Pydantic reads this once, when the class is defined, and builds a fast checker for it (written in Rust, so it's quick).&lt;/p&gt;

&lt;p&gt;After that, creating a model doesn't just store values. It runs the checker. You either get a valid object, or you get a clear error that says exactly what went wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagram: How Data Moves Through a Model&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw Input Data (dict / JSON)
        |
        v
+--------------------------------------------------------+
| Rust Core Engine (pydantic-core)                       |
|                                                        |
|  1. Check that required keys are present               |
|  2. Run 'before' field validators                      |
|  3. Check types (and convert them where possible)      |
|  4. Run 'after' field validators                       |
|  5. Run model validators                               |
+----------------------------+---------------------------+
                             |
              +--------------+--------------+
              v                             v
        [ Success ]                   [ Failure ]
   Valid model object            ValidationError raised
   (use it with dot access)      (says what failed, and where)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key Ideas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inheriting from &lt;code&gt;BaseModel&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To define your data shape, create a class that inherits from &lt;code&gt;BaseModel&lt;/code&gt; and add type annotations for each field. Attributes without an annotation are ignored by Pydantic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating a Model &amp;amp; Reading Values&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a model with keyword arguments, or unpack a dict with the double-asterisk operator (like &lt;code&gt;User(**data)&lt;/code&gt;). Once it's valid, read values with dot notation (like &lt;code&gt;user.id&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Happens When Validation Fails&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the data is bad, Pydantic raises a &lt;code&gt;ValidationError&lt;/code&gt;. Calling &lt;code&gt;.errors()&lt;/code&gt; on it gives you a list of problems. Each one tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;loc&lt;/code&gt;: &lt;em&gt;where&lt;/em&gt; the problem is (which field)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type&lt;/code&gt;: &lt;em&gt;what kind&lt;/em&gt; of error it is&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;msg&lt;/code&gt;: a readable message&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;input&lt;/code&gt;: the value that caused it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How It Compares to Alternatives&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Pydantic &lt;code&gt;BaseModel&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Python &lt;code&gt;@dataclass&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;&lt;code&gt;TypedDict&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main purpose&lt;/td&gt;
&lt;td&gt;Check data and convert types while the program runs&lt;/td&gt;
&lt;td&gt;A shortcut for writing simple data-holding classes&lt;/td&gt;
&lt;td&gt;Type hints for dictionaries (for editors and type checkers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checks data at runtime?&lt;/td&gt;
&lt;td&gt;Yes (strict or flexible)&lt;/td&gt;
&lt;td&gt;No (only if you write your own &lt;code&gt;__post_init__&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No (hints disappear at runtime)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Converts types?&lt;/td&gt;
&lt;td&gt;Yes, automatically (e.g., &lt;code&gt;"123"&lt;/code&gt; ➔ &lt;code&gt;123&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turning into dict/JSON&lt;/td&gt;
&lt;td&gt;Built in (&lt;code&gt;model_dump&lt;/code&gt;, &lt;code&gt;model_dump_json&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;You do it by hand with &lt;code&gt;asdict()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Just use &lt;code&gt;json.dumps()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code Example: Building a Model and Reading Errors&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from datetime import datetime
from pydantic import BaseModel, PositiveInt, ValidationError

# 1. Define the model (the "shape" of your data)
class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: datetime | None
    tastes: dict[str, PositiveInt]

# 2. Create a model from messy data; Pydantic converts types for you
external_data = {
    'id': '123',                     # str -&amp;gt; int
    'signup_ts': '2026-10-06 12:00', # str -&amp;gt; datetime
    'tastes': {'wine': 9, b'cheese': 7, 'cabbage': '1'}  # keys and values get converted
}

user = User(**external_data)
print(f"Validated User ID: {user.id} ({type(user.id)})")
# Output: Validated User ID: 123 (&amp;lt;class 'int'&amp;gt;)

# 3. What happens with bad data
invalid_data = {'id': 'not-an-int', 'tastes': {}}

try:
    User(**invalid_data)
except ValidationError as e:
    print("Caught ValidationError payload:")
    for err in e.errors():
        print(f" - Field {err['loc']}: {err['msg']} (Input: {err['input']})")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;h2&gt;
  
  
  2. Fields &amp;amp; Types - Setting the Rules for Each Value
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Simple Idea&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the Model is the whole contract, then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Types&lt;/strong&gt; say what &lt;em&gt;kind&lt;/em&gt; of value a field can hold (like &lt;code&gt;int&lt;/code&gt; or &lt;code&gt;str&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fields&lt;/strong&gt; add &lt;em&gt;extra rules and notes&lt;/em&gt; (like "must be greater than 0").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pydantic has two ways of reading input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lax mode (the default):&lt;/strong&gt; Pydantic is forgiving. It tries to safely convert values, like turning the string &lt;code&gt;"42"&lt;/code&gt; into the number &lt;code&gt;42&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict mode:&lt;/strong&gt; Pydantic is picky. If the value isn't &lt;em&gt;exactly&lt;/em&gt; the right Python type, it's rejected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Diagram: How a Single Field Is Checked&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input Value --&amp;gt; [ Look at the field's type ]
                          |
         +----------------+----------------+
         v                                 v
    Lax Mode                          Strict Mode
 (Try to convert,                  (Must already be
  e.g. "123" -&amp;gt; 123)                the exact type)
         |                                 |
         +----------------+----------------+
                          v
         [ Check the field's rules ]
         (gt, lt, pattern, length)
                          |
                          v
            Pass -&amp;gt; Value is saved on the object
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key Ideas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;Field()&lt;/code&gt; Function&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Field()&lt;/code&gt; lets you add extra information to a field: default values, notes, and limits. Common limits are &lt;code&gt;gt&lt;/code&gt; (greater than), &lt;code&gt;ge&lt;/code&gt; (greater or equal), &lt;code&gt;lt&lt;/code&gt; (less than), &lt;code&gt;le&lt;/code&gt; (less or equal), &lt;code&gt;min_length&lt;/code&gt;, &lt;code&gt;max_length&lt;/code&gt;, and &lt;code&gt;pattern&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotated Types&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modern Pydantic uses Python's &lt;code&gt;typing.Annotated&lt;/code&gt; to bundle a type and its rules together, so you can reuse them. For example, &lt;code&gt;Annotated[int, Field(gt=0)]&lt;/code&gt; means "an integer greater than 0".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unions &amp;amp; Discriminators&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Union (&lt;code&gt;Union[A, B]&lt;/code&gt; or &lt;code&gt;A or B&lt;/code&gt; written with the pipe symbol) means "this field can be A &lt;em&gt;or&lt;/em&gt; B".&lt;/p&gt;

&lt;p&gt;A Discriminated Union adds a "label" field (like &lt;code&gt;type: Literal['user', 'admin']&lt;/code&gt;). Pydantic reads the label and goes straight to the right model, instead of trying each option one by one until something fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aliases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An alias lets the name in your Python code differ from the name in the incoming data. For example, your code can use &lt;code&gt;snake_case&lt;/code&gt; while an API sends &lt;code&gt;camelCase&lt;/code&gt;, or the API uses a name that Python doesn't allow. Pydantic offers &lt;code&gt;alias&lt;/code&gt;, &lt;code&gt;validation_alias&lt;/code&gt; (for input), and &lt;code&gt;serialization_alias&lt;/code&gt; (for output).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Lax Mode Converts&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Expected Type&lt;/th&gt;
&lt;th&gt;Input Example&lt;/th&gt;
&lt;th&gt;Converted Result&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"100"&lt;/code&gt;, &lt;code&gt;100.0&lt;/code&gt;, &lt;code&gt;True&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;100&lt;/code&gt;, &lt;code&gt;100&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Strings are parsed; floats work only if they have no decimal part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"2026-10-06T12:00:00Z"&lt;/code&gt;, &lt;code&gt;1700000000&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;datetime&lt;/code&gt; object&lt;/td&gt;
&lt;td&gt;Accepts standard date strings (ISO 8601) and Unix timestamps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"true"&lt;/code&gt;, &lt;code&gt;"1"&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;"yes"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text matching ignores upper/lower case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"hello"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b"hello"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strings are turned into bytes (UTF-8)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code Example: Field Rules, Aliases, and Discriminated Unions&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from typing import Annotated, Literal, Union
from pydantic import BaseModel, Field

# 1. The two payment options. 'method' is the label.
class CreditCardPayment(BaseModel):
    method: Literal["credit_card"]
    card_number: str = Field(..., min_length=16, max_length=16)

class PayPalPayment(BaseModel):
    method: Literal["paypal"]
    email: str

PaymentMethod = Annotated[
    Union[CreditCardPayment, PayPalPayment],
    Field(discriminator="method")   # use 'method' to pick the right model
]

# 2. A model using field rules and an alias
class Transaction(BaseModel):
    transaction_id: str = Field(..., validation_alias="txId")  # input uses "txId"
    amount: Annotated[float, Field(gt=0, description="Amount in USD")]
    payment: PaymentMethod

# Input uses the alias name, and the 'method' label picks PayPalPayment
data = {
    "txId": "TX-9901",
    "amount": 49.99,
    "payment": {
        "method": "paypal",
        "email": "user@example.com"
    }
}

tx = Transaction(**data)
print(f"Transaction ID: {tx.transaction_id}, Payment Email: {tx.payment.email}")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;h2&gt;
  
  
  3. Validators - Your Own Custom Rules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Simple Idea&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Type checks only confirm the &lt;em&gt;shape&lt;/em&gt; of data. Real apps also have &lt;em&gt;business rules&lt;/em&gt;, like "a password must contain a symbol" or "the end date must come after the start date".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validators&lt;/strong&gt; are small functions you attach to the checking process to enforce these rules. You choose two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; is the rule about one field, or about the whole model?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mode:&lt;/strong&gt; when does it run: before type checking, after it, or wrapped around it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Diagram: The Order Validators Run In&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw Input Data
      |
      v
+---------------------------------------------------------+
| 1. @field_validator(mode='before')                      |
|    (Sees the raw, unchecked input, e.g. str/dict)       |
+----------------------------+----------------------------+
                             |
                             v
+---------------------------------------------------------+
| 2. Pydantic's own type checking                         |
+----------------------------+----------------------------+
                             |
                             v
+---------------------------------------------------------+
| 3. @field_validator(mode='after')  [DEFAULT]            |
|    (Sees the value after it has the correct type)       |
+----------------------------+----------------------------+
                             |
                             v
+---------------------------------------------------------+
| 4. @model_validator(mode='after')                       |
|    (Sees the whole finished model object)               |
+---------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key Ideas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@field_validator&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Works on one field.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mode='before'&lt;/code&gt;: Runs &lt;em&gt;before&lt;/em&gt; Pydantic checks the type. Good for cleaning up raw input (like trimming text).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mode='after'&lt;/code&gt; (default): Runs &lt;em&gt;after&lt;/em&gt; the type check passes, so the value already has the right type.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@model_validator&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Works on the whole model, so it can look at several fields together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mode='before'&lt;/code&gt;: Gets the raw dictionary before any field is processed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mode='after'&lt;/code&gt;: Gets the finished model object once every field has passed its own checks. This is the right place for rules that compare fields (like start date vs. end date).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Validator Modes at a Glance&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What It Receives&lt;/th&gt;
&lt;th&gt;Best Used For&lt;/th&gt;
&lt;th&gt;Watch Out For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;before&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The raw input value (could be anything)&lt;/td&gt;
&lt;td&gt;Cleaning up input (e.g., trimming text, splitting a list)&lt;/td&gt;
&lt;td&gt;Types haven't been checked yet, so the input might be something unexpected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;after&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The value, already the correct type&lt;/td&gt;
&lt;td&gt;Applying business rules (e.g., number limits, database lookups)&lt;/td&gt;
&lt;td&gt;Only runs if the normal type check passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wrap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The raw value plus a handler function&lt;/td&gt;
&lt;td&gt;Taking control: custom error handling, or deciding whether to run the normal check&lt;/td&gt;
&lt;td&gt;More advanced; you must call the validation step yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code Example: Field and Model Validators&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from datetime import date
from pydantic import BaseModel, Field, field_validator, model_validator

class EventBooking(BaseModel):
    event_code: str
    start_date: date
    end_date: date

    # 1. Field validator (before): clean the raw text first
    @field_validator("event_code", mode="before")
    @classmethod
    def uppercase_and_strip(cls, v: str) -&amp;gt; str:
        if isinstance(v, str):
            return v.strip().upper()
        return v

    # 2. Model validator (after): compare two fields
    @model_validator(mode="after")
    def validate_date_range(self) -&amp;gt; "EventBooking":
        if self.end_date &amp;lt; self.start_date:
            raise ValueError("end_date cannot be earlier than start_date")
        return self

# Try it out
booking = EventBooking(
    event_code="  conf-2026  ",
    start_date=date(2026, 11, 1),
    end_date=date(2026, 11, 5)
)

print(f"Sanitized Event Code: '{booking.event_code}'")
# Output: Sanitized Event Code: 'CONF-2026'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;h2&gt;
  
  
  4. Serialization &amp;amp; JSON - Turning Models Back into Plain Data
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Simple Idea&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Validation takes messy outside data and turns it into clean Python objects. &lt;strong&gt;Serialization&lt;/strong&gt; does the reverse: it turns your clean objects back into plain data you can send out, such as dictionaries, JSON strings, or API documentation.&lt;/p&gt;

&lt;p&gt;Pydantic makes sure special types like &lt;code&gt;datetime&lt;/code&gt;, &lt;code&gt;UUID&lt;/code&gt;, or &lt;code&gt;Decimal&lt;/code&gt; come out as ordinary JSON values (strings and numbers), so you can send them anywhere and read them back in later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagram: How Data Goes Out&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      Validated Model Object
               |
      +--------+---------------------+
      v                              v
 model_dump()                 model_dump_json()
 (gives a Python dict)        (gives a JSON string)
      |                              |
      +--------------+---------------+
                     v
         [ Optional export settings ]
         - exclude_unset=True
         - exclude_none=True
         - by_alias=True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key Ideas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;model_dump()&lt;/code&gt; vs &lt;code&gt;model_dump_json()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;model_dump()&lt;/code&gt; gives you a normal Python &lt;code&gt;dict&lt;/code&gt;. Types like &lt;code&gt;datetime&lt;/code&gt; or &lt;code&gt;UUID&lt;/code&gt; are kept or converted depending on your options.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;model_dump_json()&lt;/code&gt; gives you a JSON &lt;code&gt;str&lt;/code&gt; directly. It's done by the Rust core, so it's the fastest option.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Export Settings&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;exclude_unset&lt;/code&gt;: Leaves out fields you never passed in when creating the object.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exclude_defaults&lt;/code&gt;: Leaves out fields whose value equals the default.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exclude_none&lt;/code&gt;: Leaves out fields whose value is &lt;code&gt;None&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;by_alias&lt;/code&gt;: Uses the alias names (like &lt;code&gt;camelCase&lt;/code&gt;) instead of your Python field names.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;JSON Schema Generation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Model.model_json_schema()&lt;/code&gt; gives you a standard JSON Schema (OpenAPI-compatible) description of your model. Tools can use it to generate documentation and client code automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Export Settings Cheat Sheet&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Model State&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exclude_unset=True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;User(id=1)&lt;/code&gt; (where &lt;code&gt;name&lt;/code&gt; defaults to &lt;code&gt;"John"&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"id": 1}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exclude_defaults=True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;User(id=1, name="John")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"id": 1}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exclude_none=True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;User(id=1, signup_ts=None)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"id": 1, "name": "John"}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;by_alias=True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Tx(transaction_id="TX1")&lt;/code&gt; (alias &lt;code&gt;"txId"&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"txId": "TX1"}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code Example: Custom Output and Schema&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import json
from datetime import datetime
from pydantic import BaseModel, Field, field_serializer

class Article(BaseModel):
    title: str = Field(..., serialization_alias="articleTitle")
    published_at: datetime
    internal_draft: bool = False

    # Custom output format for one field
    @field_serializer("published_at")
    def serialize_dt(self, dt: datetime, _info) -&amp;gt; str:
        return dt.strftime("%Y-%m-%d")

article = Article(
    title="Pydantic First Principles",
    published_at=datetime(2026, 10, 6, 22, 0)
)

# 1. Export as a Python dict, using aliases
print("model_dump(by_alias=True):")
print(article.model_dump(by_alias=True, exclude_defaults=True))
# Output: {'articleTitle': 'Pydantic First Principles', 'published_at': '2026-10-06'}

# 2. Export straight to a JSON string
print("\nmodel_dump_json():")
print(article.model_dump_json(by_alias=True))

# 3. Generate the JSON Schema
schema = Article.model_json_schema()
print("\nGenerated JSON Schema Title:", schema['title'])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;h2&gt;
  
  
  5. Configuration - Model-Wide Settings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Simple Idea&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Field()&lt;/code&gt; sets rules for one field. &lt;strong&gt;Configuration&lt;/strong&gt; (&lt;code&gt;ConfigDict&lt;/code&gt;) sets rules for the whole model at once, such as "be strict everywhere" or "don't allow unknown keys".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagram: Where Config Fits&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input Data
    |
    v
+-------------------------------------------------------------+
| ConfigDict Settings                                         |
|                                                             |
|  * strict = True / False                                    |
|  * extra = 'forbid' | 'ignore' | 'allow'                    |
|  * str_strip_whitespace = True / False                      |
|  * frozen = True / False                                    |
+-----------------------------+-------------------------------+
                              |
                              v
            Validation runs using these settings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key Ideas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting &lt;code&gt;model_config&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inside your class, add &lt;code&gt;model_config = ConfigDict(...)&lt;/code&gt; and put your settings in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main Options&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strict&lt;/code&gt;: A global switch. When on, no field gets type conversion; values must already be the exact right type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extra&lt;/code&gt;: What to do with keys in the input that your model doesn't know about:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;'ignore'&lt;/code&gt; (default): Quietly drops them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'forbid'&lt;/code&gt;: Raises a &lt;code&gt;ValidationError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'allow'&lt;/code&gt;: Keeps them as extra attributes on the object.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frozen&lt;/code&gt;: Makes the object read-only. Trying to change a value (like &lt;code&gt;model.x = value&lt;/code&gt;) raises an error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;str_strip_whitespace&lt;/code&gt;: Automatically removes spaces at the start and end of text values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Config Options at a Glance&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config Option&lt;/th&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'ignore'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Drops unknown fields without complaining&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'forbid'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fails validation if unknown fields are given&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'allow'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keeps unknown fields (stored in &lt;code&gt;__pydantic_extra__&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turns off type conversion everywhere (&lt;code&gt;"1"&lt;/code&gt; for an &lt;code&gt;int&lt;/code&gt; fails)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frozen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Object can't be changed after it's created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;populate_by_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lets you create the object using either the Python field name OR the alias&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code Example: Strict Mode and Extra Fields&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from pydantic import BaseModel, ConfigDict, ValidationError

class StrictUserProfile(BaseModel):
    # Settings for the whole model
    model_config = ConfigDict(
        strict=True,
        extra="forbid",
        str_strip_whitespace=True,
        frozen=True
    )

    username: str
    age: int

# 1. Fails: strict=True blocks converting "30" to 30
try:
    StrictUserProfile(username="alice", age="30")  # age given as a string
except ValidationError as e:
    print("Failed strict check (string not coerced to int):")
    print(f" - Error: {e.errors()[0]['msg']}")

# 2. Fails: extra='forbid' rejects the unknown 'location' key
try:
    StrictUserProfile(username="alice", age=30, location="NYC")
except ValidationError as e:
    print("\nFailed extra check (extra key 'location'):")
    print(f" - Error: {e.errors()[0]['msg']}")

# 3. Works: spaces are stripped, and the object is read-only
profile = StrictUserProfile(username="  alice_dev  ", age=30)
print(f"\nStripped Username: '{profile.username}'")  # 'alice_dev'

try:
    profile.age = 31  # Fails because frozen=True
except ValidationError as e:
    print("Instance is immutable (frozen=True)")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;h2&gt;
  
  
  Recap in One Minute
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;One-Line Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Models&lt;/td&gt;
&lt;td&gt;A class that checks incoming data and gives you a clean object or a clear error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Fields &amp;amp; Types&lt;/td&gt;
&lt;td&gt;The rules for each value: its type, limits, aliases, and how forgiving to be&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Validators&lt;/td&gt;
&lt;td&gt;Your own functions for custom rules, on one field or across the whole model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Serialization&lt;/td&gt;
&lt;td&gt;Turning clean objects back into dicts, JSON, or schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Configuration&lt;/td&gt;
&lt;td&gt;Settings that change how the whole model behaves (strict, extra, frozen)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;This guide was prepared in Oct 2026. For a fresher/deeper guide into Pydantic, check the official Pydantic docs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
    </item>
  </channel>
</rss>
