16

I wanted to know what is the difference between:

from pydantic import BaseModel, Field

class Person(BaseModel):
    name: str = Field(..., min_length=1)

And:

from pydantic import BaseModel, constr

class Person(BaseModel):
    name: constr(min_length=1)

Both seem to perform the same validation (even raise the exact same exception info when name is an empty string). Is it just a matter of code style? Is one of them preferred over the other?

Also, if I wanted to include a list of nonempty strings as an attribute, which of these ways do you think would be better?:

from typing import List
from pydantic import BaseModel, constr

class Person(BaseModel):
    languages: List[constr(min_length=1)]

Or:

from typing import List    
from pydantic import BaseModel, Field

class Person(BaseModel):
    languages: List[str]
    
    @validator('languages', each_item=True)
    def check_nonempty_strings(cls, v):
        if not v:
            raise ValueError('Empty string is not a valid language.')
        return v

EDIT: FWIW, I am using this for a FastAPI app.

EDIT2: For my 2nd question, I think the first alternative is better, as it includes the length requirement in the Schema (and so it's in the documentation)

asmartin
  • 459
  • 1
  • 3
  • 12

3 Answers3

12

constr and Fields don't serve the same purpose.

constr is a specific type that give validation rules regarding this specific type. You have equivalent for all classic python types.

arguments of constr:

    strip_whitespace: bool = False: removes leading and trailing whitespace
    to_lower: bool = False: turns all characters to lowercase
    to_upper: bool = False: turns all characters to uppercase
    strict: bool = False: controls type coercion
    min_length: int = None: minimum length of the string
    max_length: int = None: maximum length of the string
    curtail_length: int = None: shrinks the string length to the set value when it is longer than the set value
    regex: str = None: regex to validate the string against

As you can see thoses arguments allow you to manipulate the str itself not the behaviour of pydantic with this field.

Field doesn't serve the same purpose, it's a way of customizing fields, all fields not only str, it add 18 customization variables that you can find here.

Is it just a matter of code style? Is one of them preferred over the other?

for the specific case of str it is a matter of code style and what is preferred doesn't matter, only your use-case does.

In general it is better to don't mix different syntax together and since you often need Field(), you will find it often.

A classic use case would be api response that send json object in camelCase or PascalCase, you would use field alias to match theses objects and work with their variables in snake_case.

exemple:

class Voice(BaseModel):
    name: str = Field(None, alias='ActorName')
    language_code: str = None
    mood: str = None

I personally prefer to use pydantic types to clearly separate type rules and field annotations.

Basic example:

class Car(BaseModel):
    description: Union[constr(min_length=1, max_length=64), None] = Field(
        default=None,
        example="something",
        description="Your car description",
    )

In any case you should only use one style of model structure (field, pydantic type or both toguether) for global coherence and better readability of your project.

for your 2nd question you are right, using constr is surely the best approach since the validation rule will be added into the openapi doc.

If you want to learn more about limitation and field rules enforcement check this.

Bastien B
  • 1,018
  • 8
  • 25
  • Thank you! About the second question, the problem with your solution is that it requires the list to have 1 item, and that is not what I wanted. I need to have a list which values must be non-empty strings. So these are valid: `[]`, `["english", "spanish"]`; and this isn't: `["german", ""]` . For this, I didn't found any solution to achieve it with `Field` other than using validators. But then that requirement is not auto-included in the docs. – asmartin Dec 17 '21 at 10:29
  • mood: str = None, wrong, mood:str|None = None – falselight Feb 11 '23 at 17:41
3

This link shows the methods that do and don't work for pydantic and mypy together: https://lyz-code.github.io/blue-book/coding/python/pydantic_types/#using-constrained-strings-in-list-attributes

The best option for my use case was to make a class that inherited from pydantic.ConstrainedStr as so:

import pydantic
from typing import List

...

class Regex(pydantic.ConstrainedStr):
    regex = re.compile("^[0-9a-z_]*$")

class Data(pydantic.BaseModel):
    regex: List[Regex]
    # regex: list[Regex] if you are on 3.9+
Aidan H
  • 124
  • 9
1

I'm using pydantic 1.10.4 in python 3.9 and this is the difference I observe between using constr and Field.

from pydantic import BaseModel, Field, FilePath, constr
from typing import Union, Annotated

ContactConstr = constr(regex='\d{3}-\d{3}-\d{4}')
ContactField = Annotated[str, Field(regex='\d{3}-\d{3}-\d{4}')]

class Person(BaseModel):
    contact_with_constr: ContactConstr
    contact_with_field: ContactField
    contacts_with_constr: Union[ContactConstr, list[ContactConstr]]
    contacts_with_field: Union[ContactField, list[ContactField]] # yields incorrect schema

print(Person.schema_json(indent=2))

This creates incorrect schema for the contacts_with_field, which takes a single phone number or a list of phone numbers in the form of xxx-xxx-xxxx where x is 0-9.

{
  "title": "Person",
  "type": "object",
  "properties": {
    "contact_with_constr": {
      "title": "Contact With Constr",
      "pattern": "\\d{3}-\\d{3}-\\d{4}",
      "type": "string"
    },
    "contact_with_field": {
      "title": "Contact With Field",
      "pattern": "\\d{3}-\\d{3}-\\d{4}",
      "type": "string"
    },
    "contacts_with_constr": {
      "title": "Contacts With Constr",
      "anyOf": [
        {
          "type": "string",
          "pattern": "\\d{3}-\\d{3}-\\d{4}"
        },
        {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "\\d{3}-\\d{3}-\\d{4}"
          }
        }
      ]
    },
    "contacts_with_field": {
      "title": "Contacts With Field",
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      ]
    }
  },
  "required": [
    "contact_with_constr",
    "contact_with_field",
    "contacts_with_constr",
    "contacts_with_field"
  ]
}
jung rhew
  • 800
  • 6
  • 9