Format Validation — Email, URL, and More
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
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