There is the PhoneNumber type to validate phone numbers:
from pydantic import BaseModel
from pydantic_extra_types.phone_numbers import PhoneNumber
class User(BaseModel):
name: str
phone_number: PhoneNumber
user = User(name='John', phone_number='+447911123456')
print(user.phone_number)
#> tel:+44-7911-123456
I'm satisfied with it except the unwanted "tel:" prefix. I applied a trick like that:
from pydantic import BaseModel
from pydantic_extra_types.phone_numbers import PhoneNumber
class User(BaseModel):
name: str
phone_number: PhoneNumber
@model_validator(mode="after")
def remove_prefix_from_phone(self) -> "User":
if self.phone_number:
self.phone_number = self.phone_number.removeprefix("tel:")
return self
user = User(name='John', phone_number='+447911123456')
print(user.phone_number)
#> +44-7911-123456
But problem is that I convert the field into str and the PhoneNumber class is not even considered as str subtype unlike EmailStr for example. Seems like it a bad move and MyPy marks it as error as well - incompatible types in assignment (meanwhile, the validation logic is fine).
Maybe I could do it smarter?
I couldn't think of other options but it should be done within the Pydantic model for sure, because I load data from DB in a web-service and prefix would be exist in auto-generated response.
Would be glad for any piece of advice!
The PhoneNumber
class has these class variables:
#https://github.com/pydantic/pydantic-extra-types/blob/092251d226edcf4e06bbe4f904da177fad20a6de/pydantic_extra_types/phone_numbers.py#L26
default_region_code: str | None = None
phone_format: str = 'RFC3966'
min_length: int = 7
max_length: int = 64
If we follow a bunch of code chains, we end up in the phonenumber
repository that PhoneNumber
depends on. This is the last stop for your data before being returned as a PhoneNumber
, We can see that phone_format
ultimately affects the phone number in the following ways:
#https://github.com/daviddrysdale/python-phonenumbers/blob/2f06ef6db2ca83f3856fbb8019a0c665f5971b13/python/phonenumbers/phonenumberutil.py#L1726
def _prefix_number_with_country_calling_code(country_code, num_format, formatted_number):
"""A helper function that is used by format_number and format_by_pattern."""
if num_format == PhoneNumberFormat.E164:
return _PLUS_SIGN + unicod(country_code) + formatted_number
elif num_format == PhoneNumberFormat.INTERNATIONAL:
return _PLUS_SIGN + unicod(country_code) + U_SPACE + formatted_number
elif num_format == PhoneNumberFormat.RFC3966:
#_RF3966_PREFIX = 'tel:'
return _RFC3966_PREFIX + _PLUS_SIGN + unicod(country_code) + U_DASH + formatted_number
else:
return formatted_number
It may be notable that the above returns only the formatted number if you use an unrecognized format. There is a 'NATIONAL' format that is oddly absent from the above conditions. Using it should trigger the else
.
This should solve your problem.
from pydantic import BaseModel
from pydantic_extra_types.phone_numbers import PhoneNumber
PhoneNumber.phone_format = 'E164' #'INTERNATIONAL', 'NATIONAL'
class User(BaseModel):
name: str
phone_number: PhoneNumber