Search code examples
pythonpropertiesgettersetter

Validation in a getter method


Normally when I use properties in my classes and have both getter and setter methods, I do all the validation checks inside the setter.

On the other hand, what do I do if I want to restrict the end user from changing the value of the property but still need to perform validations? I just put the checks into the getter although I'm not sure if it really belongs there.

class Foo:
    def __init__(self, value):
        self.__value = value

    @property
    def value(self):
        if not isinstance(value, int):
            raise ValueError(f'Expecting an integer, got {type(value)}.')
        else:
            return self.__value

This works fine, but the issue is the a) no validation is done until the property is accessed and b) validation is performed every time a property is accessed, which can get expensive.

So I made the below version instead. It solves the above issues but it looks and feels wrong. Is there a better way to do this:

class Foo:
    def __init__(self, val):
        self.__val = val

    @property
    def __val(self):
        return self.__tmp
    
    @__val.setter
    def __val(self, value):        
        if not isinstance(value, int):
            raise ValueError()
        else:
            self.__tmp = value

    @property
    def value(self):
        return self.__val

I guess, I could do the validation in __init__ as well, but this is also ugly.

EDIT:

>>> a = Foo('bar')

Traceback (most recent call last):
  File "C:\python\block_model_variable_imputer\venv_fixed\lib\site-packages\IPython\core\interactiveshell.py", line 3437, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-8-637c10c15d73>", line 1, in <module>
    a = Foo('bar')
  File "<ipython-input-7-f7d16143eb85>", line 3, in __init__
    self.__val = val
  File "<ipython-input-7-f7d16143eb85>", line 12, in __val
    raise ValueError()
ValueError

Solution

  • Personally, I'd go for

    class Foo:
        def __init__(self, value):
            if not isinstance(value, int):
                raise TypeError(f'Expecting an integer, got {type(value)}.')
            else:
                self.__value = value
    
        @property
        def value(self):
            return self.__value
    

    Note that I used TypeError rather than ValueError, as that's the more appropriate exception type here ;)

    This solves both of

    a) no validation is done until the property is accessed and b) validation is performed every time a property is accessed, which can get expensive.

    and also doesn't introduce an additional helper variable like in your second snippet. Having setters and getters (for __val) exclusively for internal usage seems bad practice to me …

    If the goal of your second snippet is to move the checking logic out of __init__ for readability, you could do something like

    class Foo:
        def __init__(self, value):
            self.__value = self._check_value_input(value)
    
        def _check_value_input(value):
            if not isinstance(value, int):
                raise TypeError(f'Expecting an integer, got {type(value)}.')
            else:
                return value
    
        @property
        def value(self):
            return self.__value
    

    This also has the benefit that you can reuse _check_value_input if at some point you do need to check the input at some other pace, e.g. if you do decide to re-add the setter for value.