Python attr.s multiple type validation

A class that generates a UUID by default if none is provided, and validates/creates a UUID object if a str is provided.

import attr
from attrs import validators
from uuid import UUID, uuid1

def _validate(instance, attribute, value) -> None:
    try:
        if isinstance(value, str):
            instance.uuid = UUID(value)
        return
    except Exception as e:
        raise BadUUID() from e


@attr.s(slots=True)
class Private:
    uuid = attr.ib(type=[str, UUID], validator=[validators.optional([validators.instance_of(UUID), validators.instance_of(str)]), _validate], default=uuid1())

It should with and without providing a value:

print(Private())
print(Private('d283a713-9f4b-1c15-ab8d-8d95d7ce8999'))

In the case there is none provided it should generate a new UUID using the default setting.

If value is provided it should validated it and create a UUID object.


I get an error because it will only validate one instance type, either str or UUID.

If I set instance_of(UUID) it will only work without providing a value.

And if I set it to instance_of(str) it will only work with a str being provided.


Am I doing something wrong, is there a better way to accomplish what I'm looking for?


It seems this is what you're after:

import attr
from uuid import UUID, uuid1


def _convert(value) -> UUID:
    return value if isinstance(value, UUID) else UUID(value)


@attr.s(slots=True)
class Private:
    uuid = attr.ib(default=uuid1(), converter=_convert)


p1 = Private()
print(p1.uuid)

p2 = Private('c53358b3-798e-11ec-a49b-cf6d4243e811')
print(p2.uuid)

Example result:

d5e2d087-798e-11ec-9d59-cf6d4243e811
c53358b3-798e-11ec-a49b-cf6d4243e811

If you prefer that only strings are converted and your own exception is raised if anything else was passed (although I'd probably just leave it up to UUID() itself), this works:

import attr
from uuid import UUID, uuid1


class BadUUID(Exception):
    ...


def _convert(value) -> UUID:
    if isinstance(value, str):
        return UUID(value)
    elif isinstance(value, UUID):
        return value
    else:
        raise BadUUID(f'{value} is neither a string nor a UUID')


@attr.s(slots=True)
class Private:
    uuid = attr.ib(default=uuid1(), converter=_convert)


p = Private(42)

Or if your intent was to catch exceptions from UUID and add something:

import attr
from uuid import UUID, uuid1


class BadUUID(Exception):
    ...


def _convert(value) -> UUID:
    try:
        return value if isinstance(value, UUID) else UUID(value)
    except Exception as e:
        raise BadUUID (f'{value} is not a good UUID') from e


@attr.s(slots=True)
class Private:
    uuid = attr.ib(default=uuid1(), converter=_convert)


p = Private(42)