🐍 PythonDate / Time

Date Validation in Python

Validate dates in Python — correctly handle leap years, validate date ranges, verify minimum age for GDPR / COPPA compliance, and normalise user input to ISO 8601 canonical form.

Also available in Node.js

1. Leap year rules

A year is a leap year if it is divisible by 4 — except century years, which must also be divisible by 400.

  Divisible by 4?     candidate
  Divisible by 100?   NOT a leap year (century rule)
  Divisible by 400?   IS a leap year (override)

  2000  ÷400  leap year    
  1900  ÷100 but not ÷400  NOT a leap year  
  2024  ÷4, not ÷100  leap year             
  2026  not ÷4  NOT a leap year             
⚠️2000-02-29 is valid; 1900-02-29 is not. Python's datetime handles this correctly, but many frontend validators do not. Always validate server-side.

2. API response structure

Endpoint: GET /v0/date?value=…

{
  "valid": true,
  "date": "2026-03-12",
  "year": 2026,
  "month": 3,
  "day": 12,
  "dayOfWeek": "Thursday",
  "isLeapYear": false,
  "timestamp": 1741737600000
}

timestamp is milliseconds since Unix epoch (UTC midnight). Use it for range comparisons — avoids timezone arithmetic entirely.


3. Accepted input formats

# ISO 8601 (preferred)
await iv.date("2026-03-12")

# DD/MM/YYYY (European)
await iv.date("12/03/2026")

# MM/DD/YYYY (US)
await iv.date("03/12/2026")

# Long-form
await iv.date("March 12, 2026")

# All normalise to: date = "2026-03-12"

4. Date range validation

Validate both dates in parallel with asyncio.gather, then compare timestamps.

async def validate_date_range(start: str, end: str) -> dict:
    start_res, end_res = await asyncio.gather(
        iv.date(start),
        iv.date(end),
    )

    if not start_res.valid:
        raise ValueError(f"Invalid start date: {start!r}")
    if not end_res.valid:
        raise ValueError(f"Invalid end date: {end!r}")

    # timestamp is UTC midnight in ms — safe cross-timezone comparison
    if start_res.timestamp >= end_res.timestamp:
        raise ValueError("Start date must be before end date")

    days = (end_res.timestamp - start_res.timestamp) / (1000 * 60 * 60 * 24)
    return {
        "start": start_res.date,
        "end": end_res.date,
        "days": int(days),
    }

5. Age verification (GDPR / COPPA)

ℹ️GDPR requires age 16+ for data processing consent in most EU countries (some use 13). US COPPA requires age 13+ for online services directed at children. Always use UTC for age calculations to avoid timezone edge cases on the user's birthday.
from datetime import datetime, timezone

async def verify_minimum_age(birth_date: str, min_years: int = 18) -> bool:
    result = await iv.date(birth_date)

    if not result.valid:
        raise ValueError(f"Invalid birth date: {birth_date!r}")

    # Use UTC midnight timestamps to avoid timezone bugs
    now_ts = datetime.now(timezone.utc).timestamp() * 1000  # ms
    age_ms = now_ts - result.timestamp
    age_years = age_ms / (1000 * 60 * 60 * 24 * 365.25)

    return age_years >= min_years

# FastAPI endpoint example
from fastapi import HTTPException

@app.post("/register")
async def register(birth_date: str):
    if not await verify_minimum_age(birth_date, min_years=18):
        raise HTTPException(
            status_code=422,
            detail={"error": "age_below_minimum", "min_age": 18},
        )

6. Edge cases

InputValid?Note
2000-02-29Leap year — valid
1900-02-29Century year, not ÷400 — invalid
2026-02-292026 is not a leap year
2026-04-31April has 30 days
2026-12-31Last day of year
2026-01-01First day of year
9999-12-31Far future — valid ISO 8601
0000-01-01Year 0 not in Gregorian calendar

7. Full example with asyncio

import asyncio
import os
from datetime import datetime, timezone
from isvalid_sdk import IsValidConfig, create_client

iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"]))

async def validate_date(value: str) -> dict:
    result = await iv.date(value)
    if not result.valid:
        raise ValueError(f"Invalid date: {value!r}")
    return result

# Validate a date range: start must be before end
async def validate_date_range(start: str, end: str) -> dict:
    start_res, end_res = await asyncio.gather(
        iv.date(start),
        iv.date(end),
    )
    if not start_res.valid:
        raise ValueError(f"Invalid start date: {start!r}")
    if not end_res.valid:
        raise ValueError(f"Invalid end date: {end!r}")
    if start_res.timestamp >= end_res.timestamp:
        raise ValueError("Start date must be before end date")
    return {"start": start_res.date, "end": end_res.date}

# Age verification — compare against today's UTC timestamp
async def verify_minimum_age(birth_date: str, min_years: int = 18) -> bool:
    result = await iv.date(birth_date)
    if not result.valid:
        raise ValueError(f"Invalid birth date: {birth_date!r}")

    now_ts = datetime.now(timezone.utc).timestamp() * 1000  # ms
    age_ms = now_ts - result.timestamp
    age_years = age_ms / (1000 * 60 * 60 * 24 * 365.25)
    return age_years >= min_years

async def main():
    # Leap year
    res = await iv.date("2000-02-29")
    print(res.is_leap_year)  # True

    # Range
    booking = await validate_date_range("2026-06-01", "2026-06-14")
    print(booking)  # {'start': '2026-06-01', 'end': '2026-06-14'}

    # Age check
    of_age = await verify_minimum_age("2000-03-12")
    print(of_age)  # True

asyncio.run(main())

8. Summary checklist

Accept ISO 8601, EU, US, and long-form date formats
Use timestamp (ms UTC) for range and age comparisons
Handle century years correctly (÷400 rule for leap years)
Validate both start and end dates before range comparison
Use UTC midnight for age checks — avoid timezone bugs
Enforce GDPR (16) / COPPA (13) minimum age requirements
Run parallel validation with asyncio.gather
Return 422 with reason on invalid date input

See also

Ready to integrate?

Free tier — 1,000 requests/month. No credit card required.

Get your API key →