DEV Community

Jim Grimes
Jim Grimes

Posted on • Edited on

Validating Inputs with Python Class Properties

Using Properties to Validate an Input at the Time a Class Instance is Created

The purpose of this blog post is to discuss some fundamental guidelines for creating class properties, namely: (1) the need to use getter and setter functions to validate inputs, and (2) the use of different variables for class properties and attributes. I did not have a clear understanding of those two things when I was first learning how to use class properties in python, so my attempts to use class properties were more frustrating than they needed to be. In my discussion of these points, I will refer to the following example:

Example 1:

class Person:

    def __init__(self, name):
        self.name = name

    def get_name(self):
        return self._name

    def set_name(self, new_name):
        if type(new_name) == str and len(new_name) > 0:
            self._name = new_name

    name = property(get_name, set_name)

Enter fullscreen mode Exit fullscreen mode

Example 1 establishes a Person class which is meant to satisfy these conditions:

  • An instance of the Person class must be given a name at the time of creation.
  • The property functions are used to ensure that a person's name is a string containing at least one character.
  • Calling the person's name should return the person's name.

If an instance of the Person class is created with a valid name,

> p1 = Person("Frank")
Enter fullscreen mode Exit fullscreen mode

calling the instance's name produces the following result:

> p1.name
'Frank'
Enter fullscreen mode Exit fullscreen mode

Creating an instance of the Person class with an invalid name and then calling the instance's name produces an error:

> p2 = Person(2)
> p2.name
*** AttributeError: 'Person' object has no attribute '_name'
Enter fullscreen mode Exit fullscreen mode

I am pointing out these conditions because you can find many other good examples of how to use class properties, but they will not necessarily work the same way that Example 1 does. When I was trying to learn about class properties, I tried to follow examples from various blogs but had difficulty getting the class properties to work on my own. That was because I did not recognize that the examples I found were implementing class properties in slightly different ways than what I was doing. I did not understand how the differences impacted how the class properties behaved. With that in mind, here are some important points I learned about creating class properties:

1. When the Getter and Setter Functions Are Required

In Example 1, the methods get_name and set_name are required. If you take out either method and try to set or retrieve a person's name, you will get an error:

AttributeError: can't set attribute
Enter fullscreen mode Exit fullscreen mode

I point this out because there are many blogs which say that each of the default property functions are optional. While that is technically true, it is not very helpful advice when you are trying to learn how to use a class property to validate an input at the time an instance is created.

To back up a bit: python's property function contains four default attributes, as described in the python documentation for built-in functions:

class property(fget=None, fset=None, fdel=None, doc=None)

Return a property attribute.

fget is a function for getting an attribute value. fset is a function for setting an attribute value. fdel is a function for deleting an attribute value. And doc creates a docstring for the attribute.

https://docs.python.org/3/library/functions.html#property

There are times when you do not need to use any of the default attributes. For instance:

Example 2:

class Dog:

    def __init__(self, name):
        self.name = name

    @property
    def bark(self):
        print("Woof!")
Enter fullscreen mode Exit fullscreen mode

Creating an instance of the Dog class and calling the bark property produces the following:

> d1 = Dog("Fido")
> d1.bark
Woof!
Enter fullscreen mode Exit fullscreen mode

To use the bark property of the Dog class instance, there was no need to create a get_bark or set_bark function. However, in order to validate an input as in Example 1, you must create both a getter function and a setter function for the property. The reason is that instead of name simply pointing to an assigned value, it points to the property() function. Therefore, instead of p1.name retrieving the assigned value, it calls the property's getter function, which returns the value that has been assigned to _name. Similarly, p1.name = 'Tim' doesn't directly re-assign the name attribute. Instead, it calls the property's setter function, which assigns 'Tim' to _name. If either the getter or setter function is omitted from Example 1, the class's name property cannot be set or retrieved.

2. Be Mindful About How Variable Names Are Used

In Example 1, the variable name is used in the __init__ function and has the property function assigned to it. Within get_name and set_name, the variable _name is used. You can find many other examples of class properties that use two different variables, with the only difference between the variable names being a leading underscore in one of the variables. This is a naming convention used to indicate that the variable with the leading underscore is meant to be a "private" attribute. See a discussion regarding the use of underscores here: https://stackoverflow.com/questions/1301346/what-is-the-meaning-of-single-and-double-underscore-before-an-object-name

By having the variables name and _name identical aside from the underscore, it is easier to see that they are related. Although they are related, they must use different variables. You cannot use the same variable for both the property assignment and input assignment. If I change all the attribute values in Example 1 to 'name', it gets stuck in a loop:

RecursionError: maximum recursion depth exceeded while calling a Python object
Enter fullscreen mode Exit fullscreen mode

That is because the self.name in the __init__ method calls the property() function which has been assigned to it. The property function then invokes either the get_name or set_name function. If either the get_name or set_name function uses self.name as the variable, it tries to call the property() function, which starts the loop over again. To avoid that issue, the variable used inside get_name and set_name must be distinct. It is convention to simply modify the property variable by adding an underscrore.

When learning about class properties, I was confused by many of the examples using the 'underscore' variable in the __init__ method. For instance, an example in the python documentation for built in functions uses _x as the variable in the __init__, getx, setx, and delx functions:

Example 3

class C:
    def __init__(self):
        self._x = None

    def getx(self):
        return self._x

    def setx(self, value):
        self._x = value

    def delx(self):
        del self._x

    x = property(getx, setx, delx, "I'm the 'x' property.")
Enter fullscreen mode Exit fullscreen mode

The reason it works to use _x in all the methods in Example 3 is that the value of _x is not being established at the time an instance of class C is created. Unlike the conditions for Example 1, where a person's name is provided when a Person instance is created, an instance of class C is created and, only after the instance exists, then x is assigned a value:

> c1 = C()
> c1.x
> c1.x = "value"
> c1.x
'value'
Enter fullscreen mode Exit fullscreen mode

The variable _x is given a default value of None so that, before x is assigned a value, c1.x will return None rather than causing an AttributeError.

Example 1 would not work properly if the __init__ method used the _name variable. Instead of validating the name at the time an instance is created, it would only validate the name when the name is changed. A Person instance could be created with a name of the integer 1 (which violates the rule established in the set_name function), but its name could not then be changed to the integer 2 (because a name of 2 does not satisfy the rule in the set_name function):

ipdb> p2 = Person(1)
ipdb> p2.name
1
ipdb> p2.name = 2
ipdb> p2.name
1
Enter fullscreen mode Exit fullscreen mode

But a Person class instance isn't supposed to be created in the first place if the name does not satisfy the rule in the setter function. So self._name cannot be used in Example 1's __init__ method. Instead, it must use self.name, which points to the set_name property function, so that the name provided at the time an instance is created can be validated.

Conclusion

There are many blogs and tutorials that explain how to create a class property, and they can be very helpful and informative. However, in order to get the most out of the advice they offer, it is important to be aware of how an example's use of the property function differs from how you may want to use it. The values that are provided when a class instance is created and whether those values can be changed make a difference in how the class property methods must be created, and even what default property attributes are required. Personally, once I understood the underlying reasons for variable names and property attributes, creating class properties became much easier.

Top comments (0)