Handling dates and times is notoriously difficult due to timezones, leap years, and varying formats. Pydantic (specifically V2) provides robust tools to handle this, but it requires strict discipline to prevent "naive" datetime errors in production.

Here are the best practices for implementing date and datetime fields in Pydantic models.

-----

# 1. Always Use Strong Typing (Not Strings)

Never define a date field as a `str`, even if the input is a string from a JSON payload. Use Python’s standard library types. Pydantic will automatically parse ISO 8601 strings into these objects.

- **`datetime.date`**: For birthdays, holidays, or specific calendar days (no time component).
- **`datetime.datetime`**: For timestamps, log events, or scheduled appointments.
- **`datetime.time`**: For opening hours or recurring daily events.

# 2. Enforce Timezone Awareness (Crucial)

The most common bug in backend systems is mixing **Naive** (no timezone info) and **Aware** (timezone info included) datetimes.

**Best Practice:**

- Store everything in **UTC**.
- Reject naive datetimes at the API boundary.
- Use Pydantic's `AwareDatetime` type (introduced in V2).

```python
from datetime import datetime, timezone
from pydantic import BaseModel, AwareDatetime

class Event(BaseModel):
    # BAD: Accepts naive datetimes, defaults to local time
    # created_at: datetime

    # GOOD: Ensures the datetime has timezone info
    start_time: AwareDatetime

# Usage
try:
    # This will FAIL validation because no timezone is provided
    Event(start_time=datetime(2023, 1, 1, 12, 0))
except Exception as e:
    print("Validation Error: Timezone required")

# This PASSES
Event(start_time=datetime(2023, 1, 1, 12, 0, tzinfo=timezone.utc))
```

# 3. Stick to ISO 8601 Standards

Pydantic is optimized to parse ISO 8601 formats (e.g., `YYYY-MM-DDTHH:MM:SSZ`).

- **Input:** Encourage clients to send ISO strings.
- **Output:** Pydantic serializes to ISO strings by default.
- **Avoid Custom Formats:** Avoid configuring Pydantic to parse format strings like `DD/MM/YYYY`. It makes your API brittle and region-dependent. If you *must* handle a legacy format, use a `BeforeValidator`.

# 4. Validating Business Logic (Past/Future)

Use the `@field_validator` decorator to enforce logical constraints, such as ensuring a birth date is in the past or a scheduled task is in the future.

```python
from datetime import date, timedelta
from pydantic import BaseModel, field_validator
import datetime

class UserProfile(BaseModel):
    birth_date: date

    @field_validator('birth_date')
    @classmethod
    def must_be_18(cls, v: date) -> date:
        today = date.today()
        # Calculate age roughly
        age = (today - v).days / 365.25
        if age < 18:
            raise ValueError('User must be 18 or older')
        return v
```

# 5. Handling "Now" and Defaults

**Never** set a mutable default (like `datetime.now()`) directly in the field definition. Python evaluates default arguments once at definition time, not at instantiation time.

**The Wrong Way:**

```python
class Log(BaseModel):
    timestamp: datetime = datetime.now()  # ❌ Fixed to when the server started
```

**The Right Way (Field with `default_factory`):**

```python
from pydantic import BaseModel, Field
from datetime import datetime, timezone

class Log(BaseModel):
    # ✅ Evaluated every time a model is created
    timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
```

# 6. Serialization (JSON Output)

When you convert your model to JSON (e.g., sending a response in FastAPI), Pydantic converts datetime objects to strings.

If you need a specific output format (that isn't standard ISO), strictly separate your **Internal Model** from your **Response Model**, or use a serialization serializer.

```python
from pydantic import BaseModel, field_serializer

class Meeting(BaseModel):
    when: datetime

    @field_serializer('when')
    def serialize_dt(self, dt: datetime, _info):
        # Custom format only for outgoing JSON
        return dt.strftime('%Y-%m-%d %H:%M')
```

# Summary Checklist

| Feature | Recommendation | Why? |
| --- | --- | --- |
| **Type** | `AwareDatetime` | Prevents timezone confusion. |
| **Defaults** | `Field(default_factory=...)` | Prevents stale timestamps. |
| **Validation** | `@field_validator` | Enforces logic (e.g., "date must be future"). |
| **Format** | ISO 8601 | Universal standard, reduces parsing errors. |
| **Storage** | UTC | Standardizes database entries. |

-----

# 7. Example: Putting it all together

Here is a complete, production-ready example using Pydantic V2 features.

```python
from datetime import datetime, timezone
from typing import Optional
from pydantic import BaseModel, Field, AwareDatetime, field_validator

class Appointment(BaseModel):
    # 1. Use AwareDatetime to enforce timezone
    start_time: AwareDatetime

    # 2. Use default_factory for creation times
    created_at: AwareDatetime = Field(
        default_factory=lambda: datetime.now(timezone.utc)
    )

    description: str

    # 3. Validation logic
    @field_validator('start_time')
    @classmethod
    def validate_future_date(cls, v: datetime) -> datetime:
        # Ensure comparison is timezone-aware
        if v < datetime.now(timezone.utc):
            raise ValueError('Appointment must be in the future')
        return v

# --- Testing the Model ---

# Valid Input (ISO String with Timezone)
data = {
    "start_time": "2030-12-01T14:00:00Z",
    "description": "Dentist"
}
appt = Appointment(**data)
print(f"Success: {appt.start_time.isoformat()}")

# Invalid Input (Naive time provided)
try:
    Appointment(start_time=datetime(2030, 12, 1, 14, 0), description="Fail")
except Exception as e:
    print(f"\nCaught Expected Error: {e}")
```
