I was thinking about using a pydantic class to support a builder pattern, where the object values are set, one attribute at a time, and then at the end, the object values are frozen.
To that end, is it possible in pydantic to make an object immutable after starting out mutable?
If the class is subclassed from BaseModel
, then mutability/immutability is configured by adding a Model Config inside the class with an allow_mutation
attribute set to either True
/False
.
Sample Code:
from pydantic import BaseModel, NonNegativeInt
class Person(BaseModel):
name: str
age: NonNegativeInt
class Config:
allow_mutation = False
p = Person(name='Jack', age=15)
p.name = 'Jill'
Sample Output:
TypeError: "Person" is immutable and does not support item assignment
Now, this Config
object and allow_mutation
attribute (or any of the other Model Config attributes) can also be accessed and toggled from methods of the class as:
self.__config__.allow_mutation
Sample Code:
class Person(BaseModel):
name: str
age: NonNegativeInt
def __init__(self, **data) -> None:
super().__init__(**data)
self.__config__.allow_mutation = False
def is_mutable(self):
print(self.__config__, self.__config__.allow_mutation)
p = Person(name='Jack', age=15)
p.is_mutable()
p.name = 'Jill'
Sample Output:
<class '__main__.Config'> False
Traceback (most recent call last):
File "test.py", line 18, in <module>
p.name = 'Jill'
File "pydantic/main.py", line 418, in pydantic.main.BaseModel.__setattr__
TypeError: "Person" is immutable and does not support item assignment
Putting that together, you can make a class initially mutable (allow_mutation
is True
), then set its attributes one at a time (directly or via a method), then at the end, call some method to make it immutable (allow_mutation
is False
):
Sample Code:
from pydantic import BaseModel, NonNegativeInt
class Person(BaseModel):
name: str
age: NonNegativeInt
def __init__(self, **data) -> None:
super().__init__(**data)
self.__config__.allow_mutation = True
def build(self):
self.__config__.allow_mutation = False
print('-- Create a Person --')
p = Person(name='Jack', age=15)
print(p)
print('-- It is initially mutable, can set its attributes --')
p.name = 'Jill'
p.age = 17
print(p)
print('-- Making it immutable --')
p.build()
p.name = 'Jack'
Sample Output:
-- Create a Person --
name='Jack' age=15
-- It is initially mutable, can set its attributes --
name='Jill' age=17
-- Making it immutable --
Traceback (most recent call last):
File "test.py", line 29, in <module>
p.name = 'Jack'
File "pydantic/main.py", line 418, in pydantic.main.BaseModel.__setattr__
TypeError: "Person" is immutable and does not support item assignment
Some notes about this:
Accessing the private/internal __config__
object may not be a future-proof solution. If it gets renamed or how the Config
object is used changes, then this solution breaks. It is also not mentioned in the ModelConfig section of the docs, so it's probably not intended to be used like this.
Pydantic mentions that this allow_mutation
is just "faux immutability". It basically aborts the __setattr__
call if allow_mutation
is False:
# In pydantic/main.py > BaseModel
elif not self.__config__.allow_mutation or self.__config__.frozen:
raise TypeError(f'"{self.__class__.__name__}" is immutable and does not support item assignment')
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With