8000 Add a feature for checking for a valid subtype<->supertype relationship · Issue #10462 · pydantic/pydantic · GitHub
[go: up one dir, main page]

Skip to content
Add a feature for checking for a valid subtype<->supertype relationship #10462
@mikenerone

Description

@mikenerone

Initial Checks

  • I have searched Google & GitHub for similar requests and couldn't find anything
  • I have read and followed the docs and still think this feature is missing

Description

I love Pydantic, love you folks, thank you, thank you. Ok, that's out of the way. :P

The feature

Pydantic already provides the capability to validate an object as already being valid for an arbitrary type/type-annotation via TypeAdapter.validate_python(obj, strict=True). To accomplish that validation (strict or not), it must do a pretty impressive probably-in-lockstep traversal of both the object and the type-annotation (or more accurately its schema, I'm guessing). With strict=True, this operation is very much analogous to an isinstance() check, except with deep traversal and support for generic types. That's all great stuff!

What I find myself in need of is the ability to determine if an arbitrary type/type-annotation is a valid subtype of another type/type-annotation. This is analogous to an issubclass() check, but as above, with deep traversal and support for generic types. This sounds like a bear to implement from scratch, but it also seems like the Pydantic devs have already done that work, because it should be very much like validate_python(), except that the in-lockstep traversal is between two Pydantic schemas instead of an object and a schema. IMO, this seems like a natural addition to TypeAdapter. Perhaps is_supertype() is a decent name? I'm not sure. It would return a boolean. Alternatively maybe something more along the lines of validate_subtype(), which would raise ValidationError if the check failed.

Examples

This is the usage I have in mind, but whatever interface you folks think is best is fine by me.

TypeAdapter(list[int | float]).is_supertype(list[int])  # True
TypeAdapter(list[int | float]).is_supertype(list[str])  # False

This is more like my actual use case, though stripped down to the point of unrealism. I have a generic class that takes a caller-specified type, typically a base model class, and this is the type I want to advertise for type-checking purposes. But at runtime I need the help of a discriminator for actual validation, so I want to let the caller also pass a second type to be used for that. It would be beneficial if I could assert that those passed-in types actually square with each other, i.e. that the validation type really is a proper subtype.

class Animal(BaseModel):
    kind: str

    def this_base_class_does_provide_some_important_shared_behavior(self) -> Any: ...

class Cat(Animal):
    kind: Literal["cat"] = "cat"

class Dog(Animal):
    kind: Literal["dog"] = "dog"

class Lizard(Animal):
    kind: Literal["lizard"] = "lizard"

DiscriminatedAnimal = Annotated[Cat | Dog | Lizard, Discriminator("kind")]

# Imagine that this only this bit is the code I control. The generic argument and fields are provided by a caller.
class MyGenericModel[CallerSpecifiedType](BaseModel):
    # Note: in real life, this first field is computed from the generic arg at runtime, but that's not important here.
    indicated_type: type[CallerSpecifiedType]
    validation_type: type[Any] = None

    @model_validator(mode="before")
    @classmethod
    def check_validation_type(cls, obj: Any) -> type[CallerSpecifiedType]:
        # Here is where I'd like to check that the validation type passed by the caller really checks out as a subtype
        # of the indicated type also passed by the caller. I.e. that any object valid for validation_type is guaranteed
        # to be valid for indicated_type. Something along these lines...
        if isinstance(obj, dict):
            indicated_type = obj["indicated_type"]
            validation_type = obj.get("validation_type")
            if validation_type:
                ########################################### HERE'S THE CALL ###########################################
                if not TypeAdapter(indicated_type).is_supertype(validation_type):
                    raise ValueError("validation_type is not a subtype of indicated_type")
            else:  # btw, caller can skip passing validation_type if the indicated type itself will do
                obj["validation_type"] = indicated_type
        return obj

    def example_parse_a_thing(self, thing: Any) -> CallerSpecifiedType:
        # ...so I can promise that this validation supporting the discrimination really does return an instance of
        # CallerSpecifiedType.
        return TypeAdapter(self.validation_type).validate_python(thing)

# This one would be good
instance = MyGenericModel[Animal](indicated_type=Animal, validation_type=DiscriminatedAnimal)

# Oops, I accidentally passed in DiscriminatedPlant instead of DiscriminatedAnimal, so ValidationError
instance = MyGenericModel[Animal](indicated_type=Animal, validation_type=DiscriminatedPlant)

Note: Yes, in real life I'd store and reuse those type adapters.

Note: there's an existing discussion that was opened about this possibility, but I'm opening it as a formal issue, more fleshed out (and also in the hope that it will get more attention).

Affected Components

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0