-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
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
- Compatibility between releases
- Data validation/parsing
- Data serialization -
.model_dump()
and.model_dump_json()
- JSON Schema
- Dataclasses
- Model Config
- Field Types - adding or changing a particular data type
- Function validation decorator
- Generic Models
- Other Model behaviour -
model_construct()
, pickling, private attributes, ORM mode - Plugins and integration with other tools - mypy, FastAPI, python-devtools, Hypothesis, VS Code, PyCharm, etc.