If we are using Encapsulation to manage our object attributes, we can also use getters and setters to manage them - especially if the attribute is private. For example:

class Wallet:
	def __init__(self):
		self.__money = 0

We can define getter and setter methods for accessing the private attribute __money using the @property decorator:

class Wallet:
    def __init__(self):
        self.__money = 0
 
    # A getter method
    @property
    def money(self):
        return self.__money
 
    # A setter method
    @money.setter
    def money(self, money):
        if money >= 0:
            self.__money = money

By using the @property decorator (and the @money.setter in the second definition) a client can access the __money attribute, but with some safeguards. This means that a client will not know that the attribute is private, either.

wallet = Wallet()
print(wallet.money)
 
wallet.money = 50
print(wallet.money)
 
wallet.money = -30
print(wallet.money)

Note

Parentheses are not necessary; instead it is perfectly acceptable to state wallet.money = 50, as if we were simply assigning a value to a variable. Indeed, the purpose was to hide (i.e. encapsulate) the internal implementation of the attribute while offering an easy way of accessing and modifying the data stored in the object.

Note

The getter method, i.e. the @property decorator, must be introduced before the setter method, or there will be an error when the class is executed. This is because the @property decorator defines the name of the “attribute” offerred to the client. The setter method, added with .setter, simply adds a new functionality to it.

Let’s assume this setup:

class SimpleDate:
    def __init__(self, day: int):
        self.day = day  # <--- this line is the key
 
    @property
    def day(self):
        return self.__day
 
    @day.setter
    def day(self, day: int):
        print("Setter called")  # Just for illustration
        if day < 1 or day > 30:
            raise ValueError("Invalid day")
        self.__day = day

Now when you do:

date = SimpleDate(10)

The steps are:

  1. __init__ runs.

  2. Inside __init__, the line self.day = day executes.

  3. Python sees that self.day = ... refers to a property with a setter.

  4. Instead of assigning to an instance variable, it calls:

    SimpleDate.day.__set__(self, 10)

    Or simply:

    self.day(10)  # the setter
  5. Inside the setter, the value is validated, and then the actual data is stored in the backing variable:

    self.__day = day  # safe, no recursion

So: self.day = day calls the @day.setter.


❌ What Causes Recursion?

Now, imagine if your setter did this:

@day.setter
def day(self, day: int):
    self.day = day  # ← This calls the setter again!
  • You just called the setter from inside itself → it runs again…

  • Which calls itself again…

  • And so on → infinite recursion → RecursionError.


✅ Correct Pattern

Always set the backing variable, not the property:

@day.setter
def day(self, day: int):
    self.__day = day  # Safe: sets the actual storage, not the property