Skip to content

Format Validation — Email, URL, and More

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you'll learn about Format Validation. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Format validation checks that string values match a specific pattern or structure, such as email, URL, phone number, IP address, or date formats.

What You'll Learn

By the end of this lesson, you will validate common formats (email, URL, IP, phone, date), implement custom format validators, and understand regex-based validation.

Why It Matters

Format validation catches malformed data before it causes downstream errors. An invalid email stored in your database causes delivery failures and bounces.

Real-World Use

Durga Antivirus Pro validates file hash formats (MD5, SHA256) before lookup. Invalid hashes are rejected immediately, saving database queries.

Format Validation Flow

flowchart LR
    Input[String Input] --> Email{Email?}
    Email -->|Yes| Format[Check @ and domain]
    Email -->|No| URL{URL?}
    URL -->|Yes| Check[Check scheme and TLD]

Format Validators

# format_validators.py
import re
from typing import Any, Dict, List, Optional

class FormatValidators:
    @staticmethod
    def email(value: str) -> Optional[str]:
        if not isinstance(value, str):
            return "Email must be a string"
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if not re.match(pattern, value.strip()):
            return "Invalid email format"
        return None

    @staticmethod
    def url(value: str) -> Optional[str]:
        if not isinstance(value, str):
            return "URL must be a string"
        pattern = r'^https?://[^\s/$.?#].[^\s]*$'
        if not re.match(pattern, value.strip()):
            return "Invalid URL format"
        return None

    @staticmethod
    def ipv4(value: str) -> Optional[str]:
        if not isinstance(value, str):
            return "IP must be a string"
        parts = value.strip().split(".")
        if len(parts) != 4:
            return "Invalid IPv4 format"
        for p in parts:
            try:
                num = int(p)
                if num < 0 or num > 255:
                    return "IP octet out of range (0-255)"
            except ValueError:
                return "IP octet must be numeric"
        return None

    @staticmethod
    def phone(value: str) -> Optional[str]:
        if not isinstance(value, str):
            return "Phone must be a string"
        cleaned = re.sub(r'[\s\-\(\)\+]', '', value)
        if not cleaned.isdigit() or len(cleaned) < 7 or len(cleaned) > 15:
            return "Invalid phone number format"
        return None

    @staticmethod
    def date_iso(value: str) -> Optional[str]:
        if not isinstance(value, str):
            return "Date must be a string"
        try:
            from datetime import datetime
            datetime.strptime(value.strip(), "%Y-%m-%d")
            return None
        except ValueError:
            return "Invalid date format (expected YYYY-MM-DD)"

fv = FormatValidators()
tests = [
    ("user@example.com", fv.email),
    ("not-an-email", fv.email),
    ("https://example.com", fv.url),
    ("not-a-url", fv.url),
    ("192.168.1.1", fv.ipv4),
    ("256.0.0.1", fv.ipv4),
    ("+1-555-0100", fv.phone),
    ("12", fv.phone),
    ("2024-01-15", fv.date_iso),
    ("15/01/2024", fv.date_iso),
]

for value, validator in tests:
    error = validator(value)
    status = "VALID" if not error else error
    print(f"  {str(value):25s} -> {status}")

Expected output:

  user@example.com             -> VALID
  not-an-email                 -> Invalid email format
  https://example.com          -> VALID
  not-a-url                   -> Invalid URL format
  192.168.1.1                  -> VALID
  256.0.0.1                    -> IP octet out of range (0-255)
  +1-555-0100                  -> VALID
  12                           -> Invalid phone number format
  2024-01-15                   -> VALID
  15/01/2024                   -> Invalid date format (expected YYYY-MM-DD)

Format Registry

# format_registry.py
from typing import Any, Callable, Dict, List, Optional

class FormatRegistry:
    def __init__(self):
        self.validators: Dict[str, Callable] = {}

    def register(self, name: str, validator_fn: Callable):
        self.validators[name] = validator_fn

    def validate(self, name: str, value: Any) -> Optional[str]:
        validator = self.validators.get(name)
        if not validator:
            return f"Unknown format: {name}"
        return validator(value)

registry = FormatRegistry()
registry.register("email", FormatValidators.email)
registry.register("url", FormatValidators.url)
registry.register("ipv4", FormatValidators.ipv4)

print(registry.validate("email", "hello@example.com"))
print(registry.validate("ipv4", "10.0.0.300"))
print(registry.validate("unknown", "test"))

Expected output:

None
IPv4 format is invalid
Unknown format: unknown

Custom Format Validator

# custom_format.py
import re
from typing import Any, Optional

class CustomFormat:
    @staticmethod
    def hex_color(value: str) -> Optional[str]:
        if not isinstance(value, str):
            return "Must be a string"
        if not re.match(r'^#[0-9A-Fa-f]{6}$', value.strip()):
            return "Invalid hex color (expected #RRGGBB)"
        return None

    @staticmethod
    def slug(value: str) -> Optional[str]:
        if not isinstance(value, str):
            return "Must be a string"
        if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', value):
            return "Invalid slug (lowercase, hyphens only)"
        return None

    @staticmethod
    def semver(value: str) -> Optional[str]:
        if not isinstance(value, str):
            return "Must be a string"
        pattern = r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$'
        if not re.match(pattern, value.strip()):
            return "Invalid semver (expected MAJOR.MINOR.PATCH)"
        return None

print(f"#FF5733: {CustomFormat.hex_color('#FF5733')}")
print(f"#xyz:    {CustomFormat.hex_color('#xyz')}")
print(f"my-slug: {CustomFormat.slug('my-slug')}")
print(f"My Slug: {CustomFormat.slug('My Slug')}")
print(f"1.2.3:   {CustomFormat.semver('1.2.3')}")
print(f"1.2:     {CustomFormat.semver('1.2')}")

Expected output:

#FF5733: None
#xyz:    Invalid hex color (expected #RRGGBB)
my-slug: None
My Slug: Invalid slug (lowercase, hyphens only)
1.2.3:   None
1.2:     Invalid semver (expected MAJOR.MINOR.PATCH)

Common Mistakes

1. Overly Strict Email Validation

Rejecting valid emails like user+tag@example.com or "test"@example.com. Use a reasonable pattern.

2. URL Validation Without Scheme

example.com is missing scheme. Accept both http://example.com and require scheme based on context.

3. Not Normalizing Before Validation

A phone number with spaces, dashes, and parens is valid. Normalize before pattern matching.

4. Regex Without Anchors

r'@' matches anywhere in the string. Always use ^...$ anchors for full-string validation.

5. Format vs. Existence

A valid email format does not guarantee the email exists. Separate format validation from existence checks.

Practice Questions

1. What does format validation check?

That a string matches a specific pattern: email, URL, phone, IP, date, etc.

2. What regex anchor should format patterns use?

^ at the start and $ at the end to match the entire string.

3. What is normalization?

Cleaning input before validation: removing spaces from phone numbers, converting to lowercase.

4. Why separate format from existence validation?

Format checks structure ("looks like an email"). Existence checks if it actually exists (DB lookup).

Challenge

Build a format validation library with validators for: email, URL, IPv4, IPv6, phone (E.164), date (ISO 8601), hex color, slug, UUID, and base64.

FAQ

What is the best email validation regex?

A simple pattern checking for @ and domain is sufficient for most APIs. Avoid 200-character RFC regexes.

How do I validate international phone numbers?

Use the E.164 format (+1234567890) and check digit count. Libraries like google-libphonenumber handle this.

Should I validate URL format?

Yes. Check for scheme (http/https), valid domain, and no spaces.

What is a UUID format?

8-4-4-4-12 hex digits: 550e8400-e29b-41d4-a716-446655440000.

How do I validate dates?

Parse with datetime.strptime. Accept ISO 8601. Reject ambiguous formats like 01/02/2024.

Mini Project: Format Checker

# format_checker.py
import re
from typing import Any, Dict, List, Optional

def check_format(fmt: str, value: str) -> Optional[str]:
    patterns = {
        "email": r'^[^@]+@[^@]+\.[^@]+$',
        "url": r'^https?://',
        "hex": r'^#[0-9a-fA-F]{6}$',
    }
    pat = patterns.get(fmt)
    if not pat:
        return f"unknown format {fmt}"
    return None if re.match(pat, value) else f"invalid {fmt}"

print(check_format("email", "user@example.com"))
print(check_format("email", "bad"))
print(check_format("hex", "#FF5733"))

Expected output:

None
invalid email
None

What's Next

You understand format validation. Next, learn range validation, then length validation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro