Using Getters and Setters in Python
Tags: python, oop
Published: 2019-04-23 06:24:36

---
title: Using Getters and Setters in Python
published: true
description: Writing object-oriented Python with getters and setters
tags: [Python, OOP]
canonical_url: https://warrenwong.org/using-getters-and-setters-in-python/
---

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`:
```python
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:
```python
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:
```python
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`.
```python
  @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:
```python
  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:
```python
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.

_Originally published at [warrenwong.org](https://warrenwong.org/using-getters-and-setters-in-python/) on April 12, 2019._