How to make pydantic class fields immutable?

282 Views Asked by At

I am trying to create a pydantic class with Immutable class fields (not instance fields).

Here is my base code:

from pydantic import BaseModel

class ImmutableModel(BaseModel):
    _name: str = "My Name"
    _age: int = 25

ImmutableModel._age = 26

print("Class variables:")
print(f"Name: {ImmutableModel._name}")
print(f"Age: {ImmutableModel._age}")

Output:

Class variables:
Name: My Name
Age: 26

I tried using the Config class inside my ImmutableModel to make fields immutable. But it seems like it only works for instance class fields.

class Config:
    allow_mutation = False

FYI, I use Python 3.6.13 and pydantic==1.9.2

3

There are 3 best solutions below

0
Arud Seka Berne S On BEST ANSWER

Initially, I tried to achieve creating immutable Class and Instance using pydantic module.

Now I were able to manage it using native method itself. Since this was completely defined by me and immutable, its fine to have no validation.

class ImmutableMetaclass(type):
    
    def __setattr__(cls, name, value):
        raise AttributeError("Cannot create or set class attribute '{}'".format(name))

class MyImmutableClass(metaclass=ImmutableMetaclass):
    
    # Define all allowed class attributes here
    CONSTANT_1 = 1

    def __setattr__(self, name, value):
        raise AttributeError("Cannot create or set class or instance attribute '{}'".format(name))
immutable_class = MyImmutableClass
immutable_class.CONSTANT_1 = 100
immutable_class.CONSTANT_2 = 200

immutable_instance = MyImmutableClass()
immutable_instance.CONSTANT_1 = 100
immutable_instance.CONSTANT_3 = 300

All the above code raises AttributeError.

9
sud On

The following works for me on pydantic==1.9.2.

from pydantic import BaseModel, Field

class ImmutableModel(BaseModel):
    name: str = Field(default="My Name")
    age: int = Field(default=25)
    
    class Config:
        allow_mutation = False

# Defining an object of ImmutableModel with default values for name and age
m = ImmutableModel()  

# This raises an error
m.age = 100  

That solution only works for instance variables. To make it work for class variables, one way to ensure class attributes are not mutable is to mess with the metaclass as shown below:

from pydantic import BaseModel, Field
from pydantic.main import ModelMetaclass

class ImmutableMeta(ModelMetaclass):
    IMMUTABLE_ATTRS = ['_name']

    def __setattr__(cls, name, value):
        if hasattr(cls, name):
            for attr in cls.IMMUTABLE_ATTRS:
                assert name != attr, f"Cannot modify class attribute '{attr}'"
        super().__setattr__(name, value)

class ImmutableModel(BaseModel, metaclass=ImmutableMeta):
    _name: str = 'This should not change'
    name: str = Field(default="This should not change when accessing through instance of class")

    class Config:
        allow_mutation = False

# Defining an object of ImmutableModel with default values for name and age
m = ImmutableModel()  

# This raises an error because of 'allow_mutation=False' in ImmutableModel Config
m.name = 'new name'  

# This raises an error since '_name' is not accessible from instance
m._name = 'new name' 

# This raises an error since '_name' is not modifiable
ImmutableModel._name = 'new name'  

Note that the solution above is effectively overriding pydantic internal functionality which could cause unprecedented issues depending on your implementation. Use with caution.

3
Dima Maligin On

I believe you can set the field to be frozen individualy:

from pydantic import BaseModel, Field

class ImmutableModel(BaseModel):
    _name: str = Field(default="My Name", frozen=True)
    _age: int = Field(default=25, frozen=True)