Parsers
Parsers are functions that convert string inputs into other data types, returning Maybe objects to handle potential parsing errors.
Basic Usage
from valid8r import parsers
# Parse a string to an integer
result = parsers.parse_int("42")
match result:
case Success(value):
print(f"Parsed integer: {value}") # Parsed integer: 42
case Failure(error):
print(f"Error: {error}")
# Parse a string to a float
result = parsers.parse_float("3.14")
match result:
case Success(value):
print(f"Parsed float: {value}") # Parsed float: 3.14
case Failure(error):
print(f"Error: {error}")
Available Parsers
Valid8r includes parsers for several common data types:
Security Considerations
Warning
All parsers include built-in DoS protection with early length validation. However, always enforce application-level size limits before parsing. See Production Deployment Security Guide for framework-specific examples.
Parser Input Limits
Valid8r parsers reject oversized inputs to prevent resource exhaustion:
Parser |
Max Length |
Rationale |
|---|---|---|
parse_email() |
254 chars |
RFC 5321 maximum |
parse_phone() |
100 chars |
NANP + international extensions |
parse_url() |
2048 chars |
Common browser URL limit |
parse_uuid() |
36 chars |
Standard UUID format |
parse_ip() |
45 chars |
IPv6 maximum length |
parse_datetime() |
100 chars |
ISO 8601 with microseconds |
parse_timedelta() |
200 chars |
Combined duration formats |
See also
Production Deployment Security Guide - Framework integration patterns
Secure Parser Development Guidelines - Writing secure custom parsers
Integer Parser
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Basic usage
result = parsers.parse_int("42")
match result:
case Success(value):
print(value) # 42
case Failure(_):
print("This won't happen")
# Custom error message
result = parsers.parse_int("abc", error_message="Please enter a valid number")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Please enter a valid number"
# Handles whitespace
result = parsers.parse_int(" 42 ")
match result:
case Success(value):
print(value) # 42
case Failure(_):
print("This won't happen")
# Parses integers with integer-equivalent float notation
result = parsers.parse_int("42.0")
match result:
case Success(value):
print(value) # 42
case Failure(_):
print("This won't happen")
Float Parser
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Basic usage
result = parsers.parse_float("3.14")
match result:
case Success(value):
print(value) # 3.14
case Failure(_):
print("This won't happen")
# Scientific notation
result = parsers.parse_float("1.23e-4")
match result:
case Success(value):
print(value) # 0.000123
case Failure(_):
print("This won't happen")
# Handles whitespace
result = parsers.parse_float(" 3.14 ")
match result:
case Success(value):
print(value) # 3.14
case Failure(_):
print("This won't happen")
# Integer as float
result = parsers.parse_float("42")
match result:
case Success(value):
print(value) # 42.0
case Failure(_):
print("This won't happen")
Boolean Parser
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Parse various true values
true_values = ["true", "True", "TRUE", "t", "T", "yes", "y", "1"]
for value in true_values:
result = parsers.parse_bool(value)
match result:
case Success(value):
assert value is True # All parse to True
case Failure(_):
print("This won't happen")
# Parse various false values
false_values = ["false", "False", "FALSE", "f", "F", "no", "n", "0"]
for value in false_values:
result = parsers.parse_bool(value)
match result:
case Success(value):
assert value is False # All parse to False
case Failure(_):
print("This won't happen")
# Parse invalid boolean string
result = parsers.parse_bool("maybe")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Input must be a valid boolean"
Date Parser
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
from datetime import date
# ISO format (default)
result = parsers.parse_date("2023-01-15")
match result:
case Success(value):
print(value) # date(2023, 1, 15)
case Failure(_):
print("This won't happen")
# Custom format
result = parsers.parse_date("15/01/2023", date_format="%d/%m/%Y")
match result:
case Success(value):
print(value) # date(2023, 1, 15)
case Failure(_):
print("This won't happen")
# Process date attributes
result = parsers.parse_date("Jan 15, 2023", date_format="%b %d, %Y")
match result:
case Success(value):
print(f"Year: {value.year}, Month: {value.month}, Day: {value.day}")
case Failure(_):
print("This won't happen")
DateTime Parser (Timezone-Aware)
The parse_datetime parser converts ISO 8601 datetime strings into timezone-aware datetime objects. Naive datetimes (without timezone information) are explicitly rejected for safety.
Security Note: Includes DoS protection with 100-character input limit.
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
from datetime import UTC, timedelta
# Parse datetime with Z suffix (UTC)
result = parsers.parse_datetime("2024-01-15T10:30:00Z")
match result:
case Success(dt):
print(f"DateTime: {dt}") # 2024-01-15 10:30:00+00:00
print(f"Timezone: {dt.tzinfo}") # UTC
print(f"Hour: {dt.hour}") # 10
case Failure(error):
print(f"Error: {error}")
# Parse with explicit UTC offset
result = parsers.parse_datetime("2024-01-15T10:30:00+00:00")
match result:
case Success(dt):
print(dt.tzinfo == UTC) # True
case Failure(_):
print("This won't happen")
# Parse with positive timezone offset
result = parsers.parse_datetime("2024-01-15T10:30:00+05:30")
match result:
case Success(dt):
offset = dt.utcoffset()
print(offset) # 5 hours, 30 minutes
print(offset == timedelta(hours=5, minutes=30)) # True
case Failure(_):
print("This won't happen")
# Parse with negative timezone offset
result = parsers.parse_datetime("2024-01-15T10:30:00-08:00")
match result:
case Success(dt):
offset = dt.utcoffset()
print(offset == timedelta(hours=-8)) # True
case Failure(_):
print("This won't happen")
# Parse with fractional seconds
result = parsers.parse_datetime("2024-01-15T10:30:00.123456Z")
match result:
case Success(dt):
print(dt.microsecond) # 123456
case Failure(_):
print("This won't happen")
# Reject naive datetime (no timezone)
result = parsers.parse_datetime("2024-01-15T10:30:00")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Datetime must include timezone information"
# Custom error message
result = parsers.parse_datetime("invalid", error_message="Please provide a valid ISO datetime")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Please provide a valid ISO datetime"
# Common use case: Parse API timestamp
def process_api_timestamp(timestamp_str: str):
result = parsers.parse_datetime(timestamp_str)
match result:
case Success(dt):
# Convert to UTC for storage
utc_time = dt.astimezone(UTC)
return {"success": True, "utc_time": utc_time.isoformat()}
case Failure(error):
return {"success": False, "error": error}
print(process_api_timestamp("2024-01-15T10:30:00+05:30"))
# {'success': True, 'utc_time': '2024-01-15T05:00:00+00:00'}
Supported Formats:
Z suffix for UTC:
2024-01-15T10:30:00ZExplicit UTC offset:
2024-01-15T10:30:00+00:00Positive/negative offsets:
2024-01-15T10:30:00+05:30,2024-01-15T10:30:00-08:00Fractional seconds:
2024-01-15T10:30:00.123456Z
Error Cases:
Empty/None input: “Input must not be empty”
Invalid format: “Input must be a valid ISO 8601 datetime”
Missing timezone: “Datetime must include timezone information”
Oversized input (>100 chars): “Input is too long” (DoS protection)
Timedelta Parser (Duration)
The parse_timedelta parser converts duration strings into timedelta objects. Supports simple format (90m), combined format (1h 30m), and ISO 8601 duration format (PT1H30M).
Security Note: Includes DoS protection with 200-character input limit and rejects negative durations.
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
from datetime import timedelta
# Simple format - minutes
result = parsers.parse_timedelta("90m")
match result:
case Success(td):
print(td.total_seconds()) # 5400 (90 * 60)
case Failure(_):
print("This won't happen")
# Simple format - hours
result = parsers.parse_timedelta("2h")
match result:
case Success(td):
print(td.total_seconds()) # 7200 (2 * 3600)
case Failure(_):
print("This won't happen")
# Simple format - days
result = parsers.parse_timedelta("3d")
match result:
case Success(td):
print(td.days) # 3
case Failure(_):
print("This won't happen")
# Combined format with spaces
result = parsers.parse_timedelta("1h 30m")
match result:
case Success(td):
print(td.total_seconds()) # 5400
case Failure(_):
print("This won't happen")
# Combined format without spaces
result = parsers.parse_timedelta("1h30m")
match result:
case Success(td):
print(td.total_seconds()) # 5400
case Failure(_):
print("This won't happen")
# Fully combined format
result = parsers.parse_timedelta("1d 2h 30m 45s")
match result:
case Success(td):
expected = (1 * 86400) + (2 * 3600) + (30 * 60) + 45
print(td.total_seconds() == expected) # True
case Failure(_):
print("This won't happen")
# ISO 8601 duration format
result = parsers.parse_timedelta("PT1H30M")
match result:
case Success(td):
print(td.total_seconds()) # 5400
case Failure(_):
print("This won't happen")
# ISO 8601 with days
result = parsers.parse_timedelta("P1DT2H")
match result:
case Success(td):
print(td.total_seconds()) # 93600 (1 day + 2 hours)
case Failure(_):
print("This won't happen")
# ISO 8601 with seconds
result = parsers.parse_timedelta("PT45S")
match result:
case Success(td):
print(td.total_seconds()) # 45
case Failure(_):
print("This won't happen")
# Reject negative durations
result = parsers.parse_timedelta("-90m")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Duration cannot be negative"
# Custom error message
result = parsers.parse_timedelta("invalid", error_message="Please provide a valid duration")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Please provide a valid duration"
# Common use case: Parse cache TTL
def set_cache_ttl(ttl_str: str, default_seconds: int = 3600):
result = parsers.parse_timedelta(ttl_str)
match result:
case Success(td):
return int(td.total_seconds())
case Failure(_):
return default_seconds
print(set_cache_ttl("15m")) # 900
print(set_cache_ttl("invalid")) # 3600 (default)
# Use case: Calculate deadline from duration
from datetime import datetime, UTC
def calculate_deadline(duration_str: str):
result = parsers.parse_timedelta(duration_str)
match result:
case Success(td):
deadline = datetime.now(UTC) + td
return {"success": True, "deadline": deadline.isoformat()}
case Failure(error):
return {"success": False, "error": error}
print(calculate_deadline("2h"))
# {'success': True, 'deadline': '2024-01-15T12:30:00+00:00'}
Supported Formats:
Simple:
90m,2h,45s,3dCombined (with spaces):
1h 30m,1d 2h 30m 45sCombined (no spaces):
1h30m,1d2h30m45sISO 8601:
PT1H30M,P1DT2H,PT45S,P1D
Supported Units:
d: daysh: hoursm: minutess: seconds
Error Cases:
Empty/None input: “Input must not be empty”
Invalid format: “Input must be a valid duration”
Negative duration: “Duration cannot be negative”
Oversized input (>200 chars): “Input is too long” (DoS protection)
Complex Number Parser
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Standard notation
result = parsers.parse_complex("3+4j")
match result:
case Success(value):
print(value) # (3+4j)
case Failure(_):
print("This won't happen")
# Alternative notation
result = parsers.parse_complex("3+4i")
match result:
case Success(value):
print(value) # (3+4j)
case Failure(_):
print("This won't happen")
# Just real part
result = parsers.parse_complex("5")
match result:
case Success(value):
print(value) # (5+0j)
case Failure(_):
print("This won't happen")
# Just imaginary part
result = parsers.parse_complex("3j")
match result:
case Success(value):
print(value) # (0+3j)
case Failure(_):
print("This won't happen")
Decimal Parser
The parse_decimal parser provides arbitrary-precision decimal arithmetic using Python’s Decimal type, which avoids floating-point precision issues:
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
from decimal import Decimal
# Parse a decimal value
result = parsers.parse_decimal("99.99")
match result:
case Success(value):
print(value) # Decimal('99.99')
print(type(value)) # <class 'decimal.Decimal'>
case Failure(_):
print("This won't happen")
# Precise decimal arithmetic (no floating-point errors)
result = parsers.parse_decimal("0.1")
match result:
case Success(value):
# With Decimal: 0.1 + 0.1 + 0.1 = 0.3 (exact)
total = value + value + value
print(total == Decimal('0.3')) # True
case Failure(_):
print("This won't happen")
# Compare with float precision issue
float_result = 0.1 + 0.1 + 0.1
print(float_result == 0.3) # False (floating-point error)
# Scientific notation support
result = parsers.parse_decimal("1.23E-4")
match result:
case Success(value):
print(value) # Decimal('0.000123')
case Failure(_):
print("This won't happen")
# Very large numbers with precision
result = parsers.parse_decimal("123456789012345678901234567890.123456789")
match result:
case Success(value):
print(value) # Full precision maintained
case Failure(_):
print("This won't happen")
# Use with financial calculations
def calculate_total_with_tax(price_str: str, tax_rate_str: str):
price_result = parsers.parse_decimal(price_str)
tax_result = parsers.parse_decimal(tax_rate_str)
match (price_result, tax_result):
case (Success(price), Success(rate)):
total = price * (Decimal('1') + rate)
return f"Total: ${total:.2f}"
case (Failure(error), _):
return f"Invalid price: {error}"
case (_, Failure(error)):
return f"Invalid tax rate: {error}"
print(calculate_total_with_tax("99.99", "0.0825")) # Total: $108.24
Enum Parser
from enum import Enum
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Define an enum
class Color(Enum):
RED = "RED"
GREEN = "GREEN"
BLUE = "BLUE"
# Parse to enum
result = parsers.parse_enum("RED", Color)
match result:
case Success(value):
print(value == Color.RED) # True
case Failure(_):
print("This won't happen")
# Invalid enum value
result = parsers.parse_enum("YELLOW", Color)
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Input must be a valid enumeration value"
UUID Parser
The parse_uuid parser converts string representations into UUID objects with optional version validation:
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
from uuid import UUID
# Parse a UUID string (any version)
result = parsers.parse_uuid("550e8400-e29b-41d4-a716-446655440000")
match result:
case Success(uuid_obj):
print(f"UUID: {uuid_obj}")
print(f"Version: {uuid_obj.version}") # Version: 4
case Failure(error):
print(f"Error: {error}")
# Parse with version validation (strict mode - default)
result = parsers.parse_uuid("550e8400-e29b-41d4-a716-446655440000", version=4)
match result:
case Success(uuid_obj):
print(f"Valid UUID v4: {uuid_obj}")
case Failure(_):
print("This won't happen")
# Version mismatch in strict mode
result = parsers.parse_uuid("550e8400-e29b-41d4-a716-446655440000", version=1, strict=True)
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "UUID version mismatch: expected v1, got v4"
# Non-strict mode allows version mismatch
result = parsers.parse_uuid("550e8400-e29b-41d4-a716-446655440000", version=1, strict=False)
match result:
case Success(uuid_obj):
print(f"Parsed UUID (ignoring version mismatch): {uuid_obj}")
case Failure(_):
print("This won't happen")
# Supported UUID versions: 1, 3, 4, 5, 6, 7, 8
result = parsers.parse_uuid("invalid-uuid")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Input must be a valid UUID"
# Use case: Validating API identifiers
def validate_resource_id(id_str: str):
result = parsers.parse_uuid(id_str, version=4)
match result:
case Success(uuid_obj):
return {"valid": True, "id": str(uuid_obj)}
case Failure(error) if "version mismatch" in error:
return {"valid": False, "error": "Must be a UUID v4"}
case Failure(error):
return {"valid": False, "error": error}
print(validate_resource_id("550e8400-e29b-41d4-a716-446655440000"))
# {'valid': True, 'id': '550e8400-e29b-41d4-a716-446655440000'}
Collection Type Parsing
Valid8r supports parsing strings into collection types like lists and dictionaries:
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Parse a string to a list of integers
result = parsers.parse_list("1,2,3", element_parser=parsers.parse_int)
match result:
case Success(value):
print(f"Parsed list: {value}") # Parsed list: [1, 2, 3]
case Failure(error):
print(f"Error: {error}")
# Parse with custom separator
result = parsers.parse_list("1|2|3", element_parser=parsers.parse_int, separator="|")
match result:
case Success(value):
print(f"Parsed list: {value}") # Parsed list: [1, 2, 3]
case Failure(error):
print(f"Error: {error}")
# Parse a string to a dictionary
result = parsers.parse_dict("name:John,age:30",
value_parser=parsers.parse_int)
match result:
case Success(value):
print(f"Parsed dict: {value}") # Parsed dict: {'name': 'John', 'age': 30}
case Failure(error):
print(f"Error: {error}")
# Parse a set (removes duplicates)
result = parsers.parse_set("1,2,3,2,1", element_parser=parsers.parse_int)
match result:
case Success(value):
print(f"Parsed set: {value}") # Parsed set: {1, 2, 3}
case Failure(error):
print(f"Error: {error}")
IP Address and CIDR Parsers
Valid8r provides built-in helpers for parsing IPv4, IPv6, generic IP addresses, and CIDR networks using Python’s ipaddress.
from valid8r.core.maybe import Success, Failure
from valid8r import parsers
# IPv4
match parsers.parse_ipv4("192.168.0.1"):
case Success(addr):
print(addr) # 192.168.0.1
case Failure(error):
print(error)
# IPv6 (normalized to canonical form)
match parsers.parse_ipv6("2001:db8:0:0:0:0:2:1"):
case Success(addr):
print(addr) # 2001:db8::2:1
case Failure(error):
print(error)
# Generic IP (either IPv4 or IPv6)
match parsers.parse_ip("::1"):
case Success(addr):
print(type(addr), addr) # <class 'ipaddress.IPv6Address'> ::1
case Failure(error):
print(error)
# CIDR networks (strict by default)
match parsers.parse_cidr("10.0.0.0/8"):
case Success(net):
print(net) # 10.0.0.0/8
case Failure(error):
print(error)
# Non-strict CIDR masks host bits instead of failing
match parsers.parse_cidr("10.0.0.1/24", strict=False):
case Success(net):
print(net) # 10.0.0.0/24
case Failure(error):
print(error)
Error messages are short and deterministic:
“value must be a string” for non-string inputs
“value is empty” for empty strings
“not a valid IPv4 address” or “not a valid IPv6 address” for address-specific failures
“not a valid IP address” for generic IP failures
“not a valid network” for invalid CIDR/prefix formats
“has host bits set” when strict CIDR parsing is enabled and input contains host bits
URL Parser with Structured Results
The parse_url parser validates and decomposes URLs into a structured UrlParts dataclass:
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Parse a complete URL
result = parsers.parse_url("https://user:pass@api.example.com:8080/v1/users?active=true#section")
match result:
case Success(url):
print(f"Scheme: {url.scheme}") # https
print(f"Host: {url.host}") # api.example.com
print(f"Port: {url.port}") # 8080
print(f"Path: {url.path}") # /v1/users
print(f"Query: {url.query}") # active=true
print(f"Fragment: {url.fragment}") # section
print(f"Username: {url.username}") # user
print(f"Password: {url.password}") # pass
case Failure(error):
print(f"Error: {error}")
# Simple URL without credentials
result = parsers.parse_url("https://example.com/page")
match result:
case Success(url):
print(f"Clean URL: {url.scheme}://{url.host}{url.path}")
# username and password are None
print(url.username) # None
case Failure(_):
print("This won't happen")
# URL validation use case
def validate_api_endpoint(url_str: str):
result = parsers.parse_url(url_str)
match result:
case Success(url) if url.scheme in ('http', 'https'):
return {"valid": True, "secure": url.scheme == 'https'}
case Success(url):
return {"valid": False, "error": f"Unsupported scheme: {url.scheme}"}
case Failure(error):
return {"valid": False, "error": error}
print(validate_api_endpoint("https://api.example.com/users"))
# {'valid': True, 'secure': True}
Email Parser with Structured Results
The parse_email parser validates email addresses and returns an EmailAddress dataclass with normalized components:
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Parse an email address
result = parsers.parse_email("User.Name+tag@Example.COM")
match result:
case Success(email):
print(f"Local part: {email.local}") # User.Name+tag (case preserved)
print(f"Domain: {email.domain}") # example.com (normalized lowercase)
print(f"Full: {email.local}@{email.domain}")
case Failure(error):
print(f"Error: {error}")
# Email validation for user registration
def validate_registration_email(email_str: str):
result = parsers.parse_email(email_str)
match result:
case Success(email) if email.domain in ('gmail.com', 'yahoo.com', 'outlook.com'):
return {"valid": True, "provider": email.domain}
case Success(email):
return {"valid": True, "provider": "other"}
case Failure(error):
return {"valid": False, "error": error}
print(validate_registration_email("user@gmail.com"))
# {'valid': True, 'provider': 'gmail.com'}
# Invalid email
result = parsers.parse_email("not-an-email")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Invalid email format"
Phone Number Parser (North American)
The parse_phone parser handles North American Numbering Plan (NANP) phone numbers and returns a structured PhoneNumber dataclass:
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Parse a formatted phone number
result = parsers.parse_phone("(415) 555-2671")
match result:
case Success(phone):
print(f"Area code: {phone.area_code}") # 415
print(f"Exchange: {phone.exchange}") # 555
print(f"Subscriber: {phone.subscriber}") # 2671
print(f"E.164 format: {phone.e164}") # +14155552671
print(f"National format: {phone.national}") # (415) 555-2671
case Failure(error):
print(f"Error: {error}")
# Parse with extension
result = parsers.parse_phone("415-555-2671 ext 123")
match result:
case Success(phone):
print(f"Extension: {phone.extension}") # 123
case Failure(_):
print("This won't happen")
# Different input formats - all valid
formats = [
"(415) 555-2671",
"415-555-2671",
"415.555.2671",
"4155552671",
"+1 415 555 2671",
"+1-415-555-2671"
]
for fmt in formats:
result = parsers.parse_phone(fmt)
match result:
case Success(phone):
print(f"{fmt} -> {phone.national}")
case Failure(_):
print(f"{fmt} -> INVALID")
# Canadian phone number
result = parsers.parse_phone("+1 604 555 1234", region='CA')
match result:
case Success(phone):
print(f"Region: {phone.region}") # CA
print(f"E.164: {phone.e164}") # +16045551234
case Failure(_):
print("This won't happen")
# Strict mode requires formatting characters
result = parsers.parse_phone("4155552671", strict=True)
match result:
case Success(_):
print("This won't happen in strict mode")
case Failure(error):
print(error) # "Strict mode requires formatting characters"
# Properly formatted passes strict mode
result = parsers.parse_phone("(415) 555-2671", strict=True)
match result:
case Success(phone):
print(f"Valid in strict mode: {phone.national}")
case Failure(_):
print("This won't happen")
# Validation with pattern matching guards
def validate_business_phone(phone_str: str):
result = parsers.parse_phone(phone_str)
match result:
case Success(phone) if phone.area_code in ('800', '888', '877', '866'):
return {"valid": True, "type": "toll-free"}
case Success(phone) if phone.area_code == '555':
return {"valid": False, "error": "555 is not a real area code"}
case Success(phone):
return {"valid": True, "type": "standard", "area": phone.area_code}
case Failure(error):
return {"valid": False, "error": error}
print(validate_business_phone("(800) 555-1234"))
# {'valid': True, 'type': 'toll-free'}
Creating Custom Parsers
Valid8r offers two approaches for creating custom parsers:
Using the create_parser Function
The create_parser function allows you to create a parser from any function that converts a string to another type:
from valid8r.core.parsers import create_parser
from valid8r.core.maybe import Maybe, Success, Failure
# You can still create custom parsers with create_parser for other types.
# Parse a string to an IP address
# (Built-in helpers exist: parse_ipv4/parse_ipv6/parse_ip/parse_cidr)
result = create_parser(int, "Not a valid integer")("123")
match result:
case Success(value):
print(f"Parsed: {value}") # Parsed: 123
case Failure(error):
print(f"Error: {error}")
Using the make_parser Decorator
The make_parser decorator converts a function into a parser:
from valid8r.core.parsers import make_parser
from decimal import Decimal
# Create a parser using the decorator
@make_parser
def parse_decimal(s: str) -> Decimal:
return Decimal(s)
# Parse with custom error message
@make_parser
def parse_percentage(s: str) -> float:
value = float(s.strip('%')) / 100
return value
# Use the parsers
result = parse_decimal("42.5")
match result:
case Success(value):
print(f"Parsed decimal: {value}") # Parsed decimal: 42.5
case Failure(error):
print(f"Error: {error}")
result = parse_percentage("75%")
match result:
case Success(value):
print(f"Parsed percentage: {value}") # Parsed percentage: 0.75
case Failure(error):
print(f"Error: {error}")
Validated Parsers
For cases where you want to combine parsing and validation in a single step:
from valid8r.core.parsers import validated_parser
from valid8r.core.validators import minimum, maximum
from decimal import Decimal
# Create a parser that only accepts positive numbers
positive_decimal = validated_parser(
Decimal, # Convert function
lambda x: minimum(Decimal('0'))(x), # Validator function
"Not a valid positive decimal" # Error message
)
# Use the validated parser
result = positive_decimal("42.5") # Valid
match result:
case Success(value):
print(f"Valid positive decimal: {value}") # Valid positive decimal: 42.5
case Failure(error):
print(f"Error: {error}")
result = positive_decimal("-10.5") # Invalid
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(f"Error: {error}") # Error: Value must be at least 0
Error Handling
All parsers follow consistent error handling patterns:
If the input is empty, the error is “Input must not be empty”
If the input cannot be parsed, a type-specific error is returned (e.g., “Input must be a valid integer”)
You can provide a custom error message to override the default ones
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Empty input
result = parsers.parse_int("")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Input must not be empty"
# Invalid input
result = parsers.parse_int("abc")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Input must be a valid integer"
# Custom error message
result = parsers.parse_int("abc", error_message="Please enter a number")
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Please enter a number"
Common Parser Features
All parsers have these common features:
Whitespace handling: Leading and trailing whitespace is automatically removed
Maybe return value: All parsers return a Maybe object that can be pattern matched
Custom error messages: All parsers accept an optional error_message parameter
Empty input handling: All parsers check for empty input first
Combining Parsers with Validators
Parsers are often used together with validators to create a complete validation pipeline:
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
# Parse a string to an integer, then validate it's positive
result = parsers.parse_int("42").bind(
lambda x: validators.minimum(0)(x)
)
match result:
case Success(value):
print(f"Valid positive number: {value}") # Valid positive number: 42
case Failure(error):
print(f"Error: {error}")
# Parse a string to a date, then validate it's in the future
from datetime import date
def is_future_date(d):
if d > date.today():
return Maybe.success(d)
return Maybe.failure("Date must be in the future")
result = parsers.parse_date("2025-01-01").bind(is_future_date)
match result:
case Success(value):
print(f"Valid future date: {value}") # Valid future date: 2025-01-01
case Failure(error):
print(f"Error: {error}")
Parser Function Errors vs Validation Logic
When deciding between handling errors in parser functions versus validation logic:
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
# Approach 1: Handle it in the parser directly
result = parsers.parse_int("42.5") # Will fail because it's not an integer
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Input must be a valid integer"
# Approach 2: Parse as float, then validate it's an integer
def validate_integer_value(x):
if x.is_integer():
return Maybe.success(int(x))
return Maybe.failure("Value must be an integer")
result = parsers.parse_float("42.5").bind(validate_integer_value)
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(error) # "Value must be an integer"
Parsing with Validation
Valid8r provides parser functions with built-in validation:
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Parse an integer with validation
result = parsers.parse_int_with_validation("42", min_value=0, max_value=100)
match result:
case Success(value):
print(f"Valid integer: {value}") # Valid integer: 42
case Failure(error):
print(f"Error: {error}")
# Parse a list with length validation
result = parsers.parse_list_with_validation(
"1,2,3,4,5",
element_parser=parsers.parse_int,
min_length=3,
max_length=10
)
match result:
case Success(value):
print(f"Valid list: {value}") # Valid list: [1, 2, 3, 4, 5]
case Failure(error):
print(f"Error: {error}")
# Parse a dictionary with required keys
result = parsers.parse_dict_with_validation(
"name:John,age:30,city:New York",
value_parser=parsers.parse_int,
required_keys=["name", "age"]
)
match result:
case Success(value):
print(f"Valid dict: {value}") # Valid dict: {'name': 'John', 'age': 30, 'city': 'New York'}
case Failure(error):
print(f"Error: {error}")
Parser Limitations and Edge Cases
Here are some important things to know about the parsers:
Integer Parser
Handles decimals that convert exactly to integers (e.g., “42.0”)
Rejects decimals with fractional parts (e.g., “42.5”)
Handles leading zeros (e.g., “007” → 7)
Handles large integers automatically
Float Parser
Accepts special values like “inf”, “-inf”, and “NaN”
Scientific notation is supported
Very large or small values near the limits of float precision may have representation issues
Boolean Parser
Only recognizes specific strings for true/false values
Case-insensitive for string-based inputs
Date Parser
When using custom formats, use strftime/strptime codes (e.g., %Y, %m, %d)
ISO format (YYYY-MM-DD) is the default when no format is specified
Compact formats without separators (e.g., “20230115”) need explicit format strings
Complex Parser
Handles various notations, including spaces between parts
Accepts both ‘j’ and ‘i’ for the imaginary part
Parentheses are handled (“(3+4j)” is valid)
Enum Parser
Case-sensitive by default
Works with both name and value lookup
Handles whitespace automatically
Special handling for empty values if the enum contains an empty string value
In the next section, we’ll explore validators for checking that values meet specific criteria.