pydanticの動作変更 [BaseConfig]

## はじめに

pythonは動的な型推論を行うがpydanticを使用することで型検証が可能である。しかし，作成時は型検証を行うが再代入時には型検証が行われない。この挙動を変更するには`BaseConfig`を使用する。  

## TL;DR

- `pydantic.BaseConfig`による動作の詳細設定が可能  
  - `validate_assignment`: 再代入時の型検証  
  - `allow_mutation`, `frozen`: イミュータブル化  
  - `extra`: 宣言以外のデータが入力された場合の設定  

上記以外の設定項目は[公式ドキュメント](https://pydantic-docs.helpmanual.io/usage/model_config/#options)を参照

## クラスの設定 `pydantic.BaseConfig`

BaseConfigには下記2つの記述方法がある。ここでは1つ目の記述で統一する。  

```python
# パターン1
class User_1(pydantic.BaseModel):
    id: int
    name: str

    class Config:
        validate_assignment = True
        frozen = True


# パターン2
class User_2(
    pydantic.BaseModel,
    validate_assignment=True,
    frozen=True,
):
    id: int
    name: str
```

パターン2の場合, `pydantic.BaseConfig`のデフォルト値が取得できなくなる。  
(取得できないデフォルト値を使用する場合でも正常に動作する)  

### 再代入時の型検証 `validate_assignment`

再代入時にも型検証を実施したい場合，`validate_assignment = True`を設定(デフォルト: `False`)  
  
```python:
class ValidateAssignmentModel(pydantic.BaseModel):
    id: int

    class Config:
        validate_assignment = True


user = ValidateAssignmentModel(id=0)
print(f"{user = }")

user.id = 10
print(f"{user = }")

try:
    user.id = None
except pydantic.ValidationError as e:
    print("user.id = None = ")
    print(f"{type(e).__name__}: {e}")

# user = ValidateAssignmentModel(id=0, name='abc')
# user = ValidateAssignmentModel(id=10, name='abc')
# user.id = None =
# ValidationError: 1 validation error for ValidateAssignmentModel
# id
#   none is not an allowed value (type=type_error.none.not_allowed)
```

### イミュータブル化 `allow_mutation`, `frozen`

クラスをイミュータブル化したい場合，`allow_mutation = False`もしくは`froze = True`を設定(デフォルト: `allow_mutation = True`, `froze = False`)  
クラス自体がイミュータブルになるだけなのでミュータブルな変数をイミュータブルにすることはない。  

```python:
class NotAllowMutationModel(pydantic.BaseModel):
    a: str
    b: dict

    class Config:
        allow_mutation = False
        # `allow_mutation = False` or `frozen = True`


not_allow_mutation = NotAllowMutationModel(a="hello", b={"apple": "pear"})

try:
    not_allow_mutation.a = "different"
except TypeError as e:
    print("not_allow_mutation.a = 'different' = ")
    print(f"{type(e).__name__}: {e}")

print(f'{not_allow_mutation.a = }')
print(f'{not_allow_mutation.b = }')
not_allow_mutation.b["apple"] = "grape"
print(f'{not_allow_mutation.b = }')

# not_allow_mutation.a = 'different' = 
# TypeError: "NotAllowMutationModel" is immutable and does not support item assignment
# not_allow_mutation.a = 'hello'
# not_allow_mutation.b = {'apple': 'pear'}
# not_allow_mutation.b = {'apple': 'grape'}
```

変数`b`はミュータブルな辞書型のため，クラスをイミュータブル化しても参照先の変更は保証されない。  

### 宣言以外のデータが入力された場合の設定 `extra`

宣言以外のデータが入力された場合の動作設定項目は下記3つある。上から下にいくにつれて制限が強くなる。  
宣言以外のデータが入力されたときにエラーが出力されるのは`Extra.forbid`のみ。  

- `Extra.allow`
- `Extra.ignore`
- `Extra.forbid`

```python:
class ExtraAllowModel(pydantic.BaseModel):
    a: str

    class Config:
        extra = pydantic.Extra.allow


class ExtraIgnoreModel(pydantic.BaseModel):
    a: str

    class Config:
        extra = pydantic.Extra.ignore


class ExtraForbidModel(pydantic.BaseModel):
    a: str

    class Config:
        extra = pydantic.Extra.forbid


print(f"{ExtraAllowModel(a='spam', b='oh no')  = }")
print(f"{ExtraIgnoreModel(a='spam', b='oh no') = }")
try:
    ExtraForbidModel(a="spam", b="oh no")
except pydantic.ValidationError as e:
    print("ExtraForbidModel(a='spam', b='oh no') = ")
    print(f"{type(e).__name__}: {e}")

# ExtraAllowModel(a='spam', b='oh no')  = ExtraAllowModel(a='spam', b='oh no')
# ExtraIgnoreModel(a='spam', b='oh no') = ExtraIgnoreModel(a='spam')
# ExtraForbidModel(a='spam', b='oh no') =
# ValidationError: 1 validation error for ExtraForbidModel
# b
#   extra fields not permitted (type=value_error.extra)
```

## 参考文献

- [Pydantic 公式ドキュメント](https://pydantic-docs.helpmanual.io/)
- [FastAPIを使っていなくても、Pydanticは便利です](https://attsun1031.github.io/blog/pydantic-intro)
- [Pydantic で無効なフィールドを禁止する](https://zenn.dev/ttttkkkkk/articles/ca11618e832495)