Cheatsheet for common domain types in Pydantic.

Branded IDs (type-safe)

from typing import Annotated, NewType
from pydantic import Field

UserId = Annotated[int, Field(description="User ID", ge=1)]
PostId = Annotated[int, Field(description="Post ID", ge=1)]

class Post(BaseModel):
    id: PostId
    author: UserId

Or use NewType (less ergonomic):

UserId = NewType("UserId", int)

Branding doesn’t enforce at runtime in stdlib, but Pydantic + IDE distinguishes them.

Money / Decimal

from decimal import Decimal
from pydantic import Field

class Order(BaseModel):
    amount: Decimal = Field(..., gt=0, decimal_places=2, max_digits=12)
    currency: str = Field(..., min_length=3, max_length=3, pattern=r"^[A-Z]{3}$")

Never use float for money.

Or store as integer cents:

amount_cents: int = Field(..., ge=0)

Email

from pydantic import EmailStr

class User(BaseModel):
    email: EmailStr

Requires email-validator package.

Phone

Pydantic doesn’t ship phone validation; use phonenumbers:

import phonenumbers
from typing import Annotated
from pydantic import AfterValidator

def validate_phone(v: str) -> str:
    parsed = phonenumbers.parse(v, "US")
    if not phonenumbers.is_valid_number(parsed):
        raise ValueError("invalid phone")
    return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)

Phone = Annotated[str, AfterValidator(validate_phone)]

class User(BaseModel):
    phone: Phone

URL

from pydantic import HttpUrl, AnyUrl, PostgresDsn, RedisDsn

class Config(BaseModel):
    api: HttpUrl
    db: PostgresDsn
    cache: RedisDsn

HttpUrl enforces http(s); AnyUrl allows any.

Datetime variants

from datetime import datetime
from pydantic import AwareDatetime, NaiveDatetime, PastDatetime, FutureDatetime

class Event(BaseModel):
    when: AwareDatetime          # must have tzinfo
    when_naive: NaiveDatetime    # must NOT have tzinfo
    born: PastDatetime
    expires: FutureDatetime

Date / Time

from datetime import date, time, timedelta
from pydantic import PastDate, FutureDate

class M(BaseModel):
    dob: PastDate
    starts: FutureDate
    duration: timedelta
    open_at: time

UUID

from uuid import UUID, uuid4

class Doc(BaseModel):
    id: UUID = Field(default_factory=uuid4)

For UUID v7 (sortable): use a library or generate manually.

IP addresses

from pydantic import IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork
from ipaddress import IPv4Address, IPv6Address

class Visitor(BaseModel):
    ip: IPvAnyAddress
    network: IPvAnyNetwork | None = None

File / Path

from pydantic import FilePath, DirectoryPath, NewPath

class Settings(BaseModel):
    config: FilePath          # must exist + be a file
    cache_dir: DirectoryPath  # must exist + be a dir
    output: NewPath           # must NOT exist

Color

from pydantic_extra_types.color import Color

class Theme(BaseModel):
    primary: Color            # accepts "#ff0", "rgb(255,...)", named, etc.

pydantic-extra-types package; many domain types.

Country / Language codes

from pydantic_extra_types.country import CountryAlpha2, CountryAlpha3
from pydantic_extra_types.language_code import LanguageAlpha2

class User(BaseModel):
    country: CountryAlpha2     # "US"
    language: LanguageAlpha2   # "en"

Money (extra-types)

from pydantic_extra_types.payment import PaymentCardNumber

class Card(BaseModel):
    number: PaymentCardNumber  # validates Luhn

ISBN

from pydantic_extra_types.isbn import ISBN

class Book(BaseModel):
    isbn: ISBN

Coordinate

from pydantic_extra_types.coordinate import Latitude, Longitude

class Place(BaseModel):
    lat: Latitude              # -90 to 90
    lng: Longitude             # -180 to 180

TimeZone

import pytz
from typing import Annotated
from pydantic import AfterValidator

def validate_tz(v: str) -> str:
    pytz.timezone(v)           # raises if invalid
    return v

TimeZone = Annotated[str, AfterValidator(validate_tz)]

class User(BaseModel):
    tz: TimeZone = "UTC"

Constrained collections

from typing import Annotated
from pydantic import Field

Tags = Annotated[list[str], Field(min_length=1, max_length=10)]

class Post(BaseModel):
    tags: Tags

Common mistakes

  • Storing money as float — rounding errors.
  • Generic str for phone / URL / email — accepts garbage.
  • Naive datetime in timezone-sensitive APIs — bugs at DST.
  • Branded IDs without TypedDict / NewType — not actually distinct at runtime.

Read this next

If you want my domain-type library (Phone, Money, etc.), it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .