Имеем на входе следующую модель:

from ipaddress import IPv4Address, IPv6Address, ip_address

from pydantic import BaseModel, ValidationError, field_validator
from pydantic_core import PydanticCustomError, InitErrorDetails


class Model(BaseModel):
    first_ip: IPv4Address | IPv6Address
    other_ips: list[IPv4Address | IPv6Address]
    title: str


При невалидных данных получаем абсолютно не читаемые сообщения об ошибках:

Model(first_ip='127.0.259.1', other_ips=['127.258.0.1', '2001:0db8:85a3:0000:0000:8x2e:0370:7334'], title='some title')

# pydantic_core._pydantic_core.ValidationError: 6 validation errors for Model
# first_ip.lax-or-strict[lax=function-plain[ip_v4_address_validator()],strict=json-or-python[json=function-after[IPv4Address(), str],python=is-instance[IPv4Address]]]
#   Input is not a valid IPv4 address [type=ip_v4_address, input_value='127.0.259.1', input_type=str]
# first_ip.lax-or-strict[lax=function-plain[ip_v6_address_validator()],strict=json-or-python[json=function-after[IPv6Address(), str],python=is-instance[IPv6Address]]]
#   Input is not a valid IPv6 address [type=ip_v6_address, input_value='127.0.259.1', input_type=str]
# other_ips.0.lax-or-strict[lax=function-plain[ip_v4_address_validator()],strict=json-or-python[json=function-after[IPv4Address(), str],python=is-instance[IPv4Address]]]
#   Input is not a valid IPv4 address [type=ip_v4_address, input_value='127.258.0.1', input_type=str]
# other_ips.0.lax-or-strict[lax=function-plain[ip_v6_address_validator()],strict=json-or-python[json=function-after[IPv6Address(), str],python=is-instance[IPv6Address]]]
#   Input is not a valid IPv6 address [type=ip_v6_address, input_value='127.258.0.1', input_type=str]
# other_ips.1.lax-or-strict[lax=function-plain[ip_v4_address_validator()],strict=json-or-python[json=function-after[IPv4Address(), str],python=is-instance[IPv4Address]]]
#   Input is not a valid IPv4 address [type=ip_v4_address, input_value='2001:0db8:85a3:0000:0000:8x2e:0370:7334', input_type=str]
# other_ips.1.lax-or-strict[lax=function-plain[ip_v6_address_validator()],strict=json-or-python[json=function-after[IPv6Address(), str],python=is-instance[IPv6Address]]]
#   Input is not a valid IPv6 address [type=ip_v6_address, input_value='2001:0db8:85a3:0000:0000:8x2e:0370:7334', input_type=str]


Как видим, на невалидный адрес получаем по два сообщения с ошибкой и куча перегруженной информации. Можно избавиться от этого с помощью кастомного валидатора, к примеру:

def validate_ipaddress(raw_ipaddress: int | str | None) -> int | str | None:
    if raw_ipaddress is not None:
        try:
            ip_address(raw_ipaddress)
        except ValueError as exc:
            raise PydanticCustomError('ip_address', 'Input is not a valid IPv4 address or IPv6 address') from exc

    return raw_ipaddress


def validate_ipaddresses(raw_ipaddresses: list[int | str]) -> list[int | str]:
    errors: list[InitErrorDetails] = []

    # https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.InitErrorDetails
    for index, raw_ipaddress in enumerate(raw_ipaddresses):
        try:
            validate_ipaddress(raw_ipaddress)
        except PydanticCustomError as ip_address_exc:
            errors.append(
                InitErrorDetails(
                    type=ip_address_exc,
                    input=raw_ipaddress,
                    loc=(index,),
                ),
            )

    # https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.ValidationError.from_exception_data
    if errors:
        raise ValidationError.from_exception_data(title='ip_address_error', line_errors=errors)

    return raw_ipaddresses


class Model(BaseModel):
    first_ip: IPv4Address | IPv6Address
    other_ips: list[IPv4Address | IPv6Address]
    title: str

    _validate_ipaddress = field_validator('first_ip', mode='before')(validate_ipaddress)
    _validate_ipaddresses = field_validator('other_ips', mode='before')(validate_ipaddresses)


Смотрим как изменились сообщения об ошибках:

# pydantic_core._pydantic_core.ValidationError: 3 validation errors for Model
# first_ip
#   Input is not a valid IPv4 address or IPv6 address [type=ip_address, input_value='127.0.259.1', input_type=str]
# other_ips.0
#   Input is not a valid IPv4 address or IPv6 address [type=ip_address, input_value='127.258.0.1', input_type=str]
# other_ips.1
#   Input is not a valid IPv4 address or IPv6 address [type=ip_address, input_value='2001:0db8:85a3:0000:0000:8x2e:0370:7334', input_type=str]


Хочу обратить внимание:

  • Валидация IPv4Address и IPv6Address под капотом в pydantic сделана немного косовато и отдельно от остальных типов
  • Провалидировать адрес за один проход можно с помощью ip_address()
  • Для формирования адекватного пути ошибки в составных типах можно использовать InitErrorDetails и указания относительного loc, pydantic умный и сформирует корректный абсолютный путь ошибки