Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic method to create Strict<type> fields #780

Closed
haizaar opened this issue Aug 26, 2019 · 9 comments
Closed

Generic method to create Strict<type> fields #780

haizaar opened this issue Aug 26, 2019 · 9 comments
Labels
feature request help wanted Pull Request welcome

Comments

@haizaar
Copy link
Contributor

haizaar commented Aug 26, 2019

Feature Request

This is a followup from tiangolo/fastapi#453 (comment)

Description

We'd like have a general way to create Strict field classes, e.g. StrictInt, StrictFloat, etc. that do value instance type checking during validation phase.

Rationale

My usecase for this feature is to be able properly support Unions for native types. Today if you have a field coming from JSON data that can be either bool, str, or int, you can't define a model in pydantic that will keep original data field's type.

Here is an example:

class MyModel(BaseModel):
    field: Union[int, bool, str]

The above will:

  • parse `{"field": "1"} as integer
  • parse {"field": "foo"} as boolean, since bool "eats" anything (as well as str)

I.e. MyModel(**json.loads('{"field": "foo"}')).json() != '{"field": "foo"}'

Implementation

@dmontagu suggested two approaches to implement this.

Creator function

T = TypeVar("T")

def strict_type(type_: Type[T]) -> Type[T]:
    class StrictType(type_):
        @classmethod
        def __get_validators__(cls) -> Generator[Callable[[Any], T], None, None]:
            yield cls.validate

        @classmethod
        def validate(cls, v: Any) -> T:
            if not isinstance(v, type_):
                raise TypeError(f"Expected instance of {type_}, got {type(v)} instead")
            return v
    return StrictType

StrictInt = strict_type(int)

It works just fine. However mypy does not like dynamic types and prints errors for the above code:

/tmp/foo.py:10: error: Variable "type_" is not valid as a type
/tmp/foo.py:10: error: Invalid base class "type_"
/tmp/foo.py:27: error: Variable "union2.StrictInt" is not valid as a type

Generic class

T = TypeVar("T")

class StrictType(Generic[T]):
    type_: ClassVar[Type[T]]
    def __get_validators__(cls) -> Generator[Callable[[Any], T], None, None]:
        # yield int_validator
        yield cls.validate

    @classmethod
    def validate(cls, v: Any) -> T:
          if not isinstance(v, cls.type_):
              raise TypeError(f"Expected instance of {type_}, got {type(v)} instead")
        return v

class StrictInt(StrictType[int]):
    type_ = int

This version is mypy friendly but is less convenient to the user - requires to create classes in a bit of a magical manner.

@samuelcolvin
Copy link
Member

I'm happy with StrictInt and StrictFloat and also a generic version. I'm not sure it'll always be acceptable to simply use isinstance(), for instance: isinstance(True, int) == True; we'll need other specific checks for some types. For performance reasons it would therefore be best to have custom types for common types

However I would note the following:

@haizaar
Copy link
Contributor Author

haizaar commented Aug 27, 2019

Great. Thanks for pointing out "bool is subclass of int" legacy. I was not aware of that and now understand the reason behind https://github.com/samuelcolvin/pydantic/blob/5f634067daa0236632af22c0a8b15dac310b18a1/pydantic/validators.py#L105

Let's be practical then. My original intent was to solve Union[int, bool, str] usecase. It's true that per #617, bool will not eat "foo" but it will still eat 1 (int) and "1" (string). Hence I still need StrictBool (which already exists) and currently lacking StrinctInt.
Will you accept PR for explicit implementations for StrictInt and StrictFloat to complement existing Strict* implementations or will it have to wait till v1 as well?

We can delay the generic case till there will be more demand for it (and there is already a prototype for people to borrow and play with).

descriminator is great, but I'm working with the data coming from a 3rd party and would still be able to parse it properly with pydantic.

@samuelcolvin
Copy link
Member

Will you accept PR for explicit implementations for StrictInt and StrictFloat to complement existing Strict* implementations or will it have to wait till v1 as well?

We're working on v1 now. Only bugs are being released until we get v1 out. I will accept a PR for StrictInt and StrictFloat to be included in v1.

descriminator could be a function, it won't need to be a field name, in fact it might always be a function. We need to discuss more how it'll work, but in theory it should be able to accomplish what you want here.

@DerRidda
Copy link
Contributor

DerRidda commented Sep 9, 2019

I did a PR that addresses this issue in some respect: #799

@skewty
Copy link
Contributor

skewty commented Dec 9, 2019

Whatever solution is accepted for #1060 may affect this so linking here.

@and-semakin
Copy link

Is there a way to create strict types using Field() object?

For example this is completely what I want but mypy can't parse it correctly and I would avoid using # type: ignore:

class Kek(BaseModel):
    value: conint(strict=True, ge=0, le=10)  # type: ignore

Is it possible to do it using Field or in some other way just to create strict int and avoid # type: ignore comments?

class Kek(BaseModel):
    value: int = Field(..., strict=True, ge=0, le=10)  # doesn't work, just an example/suggestion

@PrettyWood
Copy link
Member

If this can help there is a possible solution here #2079 (comment)

@tuukkamustonen
Copy link

tuukkamustonen commented May 5, 2021

An alternative view on this: Developers are most likely split into two camps:

  1. Those who want strict parsing everywhere, and
  2. those who don't.

Field-specific ability to enable strict parsing might be useful to some,but I believe masses would just want to toggle "strictness" on and write:

class Kek(BaseModel):
    f1: str
    f2: bool
    f3: int

Rather than more explicit:

class Kek(BaseModel):
    f1: StrictStr
    f2: bool = Field(..., strict=True)  # won't work, as `strict` not supported here, btw.
    f3: conint(strict=True)  # mypy will nag about this, btw.

Latter looks ugly and kludgy. On a larger project, multiply that by 100-1000+ and you have lots of wasted characters (and clarity).

@Kludex
Copy link
Member

Kludex commented Apr 25, 2023

On V2, you can use the Strict metadata. Check how it's done internally:

@_dataclasses.dataclass
class Strict(_fields.PydanticMetadata):
strict: bool = True
def __hash__(self) -> int:
return hash(self.strict)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BOOLEAN TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
StrictBool = Annotated[bool, Strict()]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request help wanted Pull Request welcome
Projects
None yet
Development

No branches or pull requests

8 participants