Using Getters and Setters in Python

Fri Apr 12 2019

by pawel czersinski on unsplash

After writing object-oriented Python for a while, there comes a point you probably need to write some getters and setters. Unlike C++, the convention in Python is to get and set the class instance property directly if there's no need for writing a setter.

For example, if we have a Rectangle class that has height and width properties and instance methods area and perimeter:

class Rectangle:
  def __init__(self, height, width):
    self.height = height
    self.width = width
  
  def area(self):
    return self.height * self.width

  def.perimeter(self):
    return (self.height + self.width) * 2

If we never needed to check the values of height and width, this would be fine. For anything like writing experiments in a Jupyter Notebook, this maybe fine, but if we wanted this code to become a part of something with a longer lifespan like a library, we probably want to have something a bit more robust.

There are some things we know about a rectangle that we should probably check for. Rectangles probably should only have positive heights and widths so we should probably check for that. They probably shouldn't be zero either, since that would make it either a point (both height and width is zero) or a line (either height or width is a zero).

For example, we could directly access the height and width but we can also directly change those properties to some value we know is invalid:

rect = Rectangle(3, 4) # Rectangle(height=3, width=4)
rect.height # 3
rect.width # 4
rect.height = 0 # Should be invalid
rect.width = -5 # Should also be invalid

Let's change up the code and use Python getters and setters without changing our API interface by updating the __init__ method and adding getters and setters with the Python decorators @property for the getters and @height.setter and @width.setter for setting the height and width respectively:

class Rectangle:
  def __init__(self, height, width):
    self._height = height
    self._width = width

  @property
  def height(self):
    return self._height

  @property
  def width(self):
    return self._width
  
  @height.setter
  def height(self, new_height):
    self._height = new_height

  @width.setter
  def width(self, new_width):
    self.width = new_width

...

So far there's no new functionality. We can still set the properties to some invalid value, but we can already see where we can check for valid values--in the method marked with @___.setter.

  @height.setter
  def height(self, new_height):
    if new_height <= 0:
      return
    self._height = new_height

  @width.setter
  def width(self, new_width):
    if new_width <= 0:
      return
    self.width = new_width

So after updating the setters, we won't be able to set the _height and _width properties to something invalid. But there's still an issue. We can still initialize our rectangle's _height and _width to invalid values. What we want to do is to use the setters in our __init__ method also. Let's also raise and exception rather than returning silently:

  def __init__(self, height, width):
    self.height = height
    self.width = width
...

  @height.setter
  def height(self, new_height):
    if new_height <= 0:
      raise Exception
    self._height = new_height

  @width.setter
  def width(self, new_width):
    if new_width <= 0:
      raise Exception
    self.width = new_width

Lets' also clean up some duplication by creating a static method to check the validity of height and width values. The final code should look like this with a new static method check_value using the @staticmethod decorator:

class Rectangle:
  def __init__(self, height, width):
    self.height = height
    self.width = width

  @property
  def height(self):
    return self._height

  @property
  def width(self):
    return self._width

  @height.setter
  def height(self, new_height):
    Rectangle.check_value(new_height)
    self._height = new_height

  @width.setter
  def width(self, new_width):
    Rectangle.check_value(new_width)
    self._width = new_width

  def area(self):
    return self.height * self.width

  def perimeter(self):
    return (self.height + self.width) * 2

  @staticmethod
  def check_value(value):
    if value <= 0:
      raise Exception

Python's approach with getters and setters is to allow you to keep writing simple programs and only having to use getters and setters when appropriate without changing the interface. This is a pretty elegant approach especially for prototyping without fear of changing too much code later when experiments prove themselves out later when you need to productionize the code.

If you found the post helpful and would like this same content delivered to you,

Subscribe to my newsletter

;