Python Getters, Setters and @property Decorator

Python Getters, Setters and @property Decorator
In: Python
Table of Contents

When I first started working with Python classes, some of the most confusing topics were getters, setters, and @property. There are plenty of tutorials on how to use them, but very few actually explain why do we need them or what problem do they solve. So, I thought I’d write a dedicated post covering what they are and the problems they solve. Let’s get to it.

As always, if you find this post helpful, press the ‘clap’ button. It means a lot to me and helps me know you enjoy this type of content.

Python OOP - Method vs Function and the Mystery of ‘self’
I just realized how much I didn’t understand about Python Object-Oriented Programming. I thought I knew the basics, but a few days ago, while going through a Python course, I found out I was wrong.

Python Classes

Before diving in, let's have a quick look at a Python class. Here’s a simple example of a Person class with two attributes name and age.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

I'm going to create an instance of the class called p1, passing a name and age as shown below.

p1 = Person('John', 36)

And as you might have guessed, I can access the attributes name and age like this.

p1.name  # Output: 'John'
p1.age   # Output: 36

I can also modify the attributes using the same syntax.

p1.name = 'Max'
p1.name  # Output: 'Max'

This is pretty basic and works just fine. So, what's the issue, you may ask?

Some Problems

Let's say down the line, you're making some improvements to your code and realize you need some control over how attributes are assigned. You don’t want someone to accidentally assign an integer to name or a string to age. You also want to make sure the age doesn’t go above 150, for example. So, what can we do here?

In some programming languages, there’s a concept of private variables, which means certain attributes can only be accessed or modified through specific methods. Python doesn’t have true private variables, even if you prepend an attribute with an underscore (_). This is just a convention, not actual enforcement.

For example, as of now, nothing is stopping me from doing this.

p1.name = 10
p1.age = 'Alice'

This obviously doesn’t make sense, so let’s say we decide to put some logic in place to prevent it. We could change our code to introduce getter and setter methods, which allow us to control how attributes are accessed and modified. Here’s how we can do it.

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    def get_name(self):
        return self._name
    
    def set_name(self, name):
        if not isinstance(name, str):
            raise ValueError('Name must be a string')
        else:
            self._name = name

    def get_age(self, age):
        self._age = age
    
    def set_age(self):
        if not isinstance(self._age, int):
            raise ValueError('Age must be an integer')
        else:
            return self._age

The key change here is that instead of directly assigning values to name and age, we are now using _name and _age with getter and setter methods. This allows us to add checks when setting values. For example, in set_name, we ensure that only strings are assigned, and in get_age, we check that the stored value is an integer.

Now, we can initialize an object as usual, but the only way to access or modify the attributes is through the class methods.

p1 = Person('John', 36)

p1.get_name()  # Output: 'John'
p1.set_name('Max')
p1.get_name()  # Output: 'Max'

However, we can no longer access attributes directly like p1.name. If we try, we’ll get an error:

p1.name  # Output: AttributeError: 'Person' object has no attribute 'name'

That said, as mentioned earlier, Python doesn’t have true private variables. If someone really wants to access _name, they can still do it like this.

p1._name  # Output: 'Max'

Even though the underscore is a convention signaling that it’s intended to be private, Python doesn’t enforce it. Of course, nothing is stopping someone from assigning values directly like this:

p1._name = 55

This will work without any errors, but generally, users should understand that if an attribute is prepended with an underscore (_), it is meant to be treated as private and should not be accessed or modified directly.

With this approach, there’s still another problem. Users can still assign values using p1.name = 'Max' or p1.age = 30. When they do this, Python doesn’t update _name or _age. Instead, it creates new attributes name and age on the object, completely bypassing our validation logic.

This means that if we access p1.get_name(), it will return _name, but if we access p1.name, it will return the newly created attribute, leading to inconsistent behavior. This is an issue because users may unknowingly bypass the validation we implemented.

For example, if someone does p1.name = 10, Python will simply create a new attribute name with the value 10, and our setter method will never be called. This completely defeats the purpose of having getter and setter methods in the first place.

What Is Python concurrent.futures? (with examples)
Python’s concurrent.futures module simplifies concurrent programming by providing a high-level interface for asynchronously executing callables.

What Does Python Recommend?

Even though the previous method works, Python actually recommends using dot notation to access attributes. You might not even have a use case for getters or setters when you're first developing your code. The more Pythonic way is to use dot notation, like this.

p1.name
p1.age

But the problem arises when, down the line, you realize you need to add some logic or checks when setting values. If you've been using dot notation and then decide to implement getter and setter methods, you now have to go back and tell all users, "Hey, if you've been using p1.name to access attributes, from now on, you need to use p1.get_name() instead."

Is this a good practice? Not really. This change would break a lot of existing code. This is where the @property decorator comes into play.

@property Decorators

Now let's look at a way to start our class with dot notation attributes while allowing us to introduce getters and setters later without breaking existing code.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

So, we can start our code like this (shown above), and users will naturally use p1.name to access attributes. Later, if we need to add validation or logic, we can easily introduce the @property decorator (shown below) without breaking any existing code.

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, name):
        if not isinstance(name, str):
            raise ValueError('Name must be a string')
        else:
            self._name = name
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, age):
        if not isinstance(age, int):
            raise ValueError('Age must be an integer')
        else:
            self._age = age

Instead of creating separate get_name and set_name methods, we are now using @property and @property.setter to define how the attributes should be accessed and modified.

  • The @property decorator makes name and age readable using dot notation, even though they are internally stored as _name and _age.
  • The @name.setter and @age.setter decorators define how values should be assigned, allowing us to add validation checks while keeping dot notation usage intact.

This means we can still initialize a Person object, access attributes with dot notation, and assign values the same way we did before. But now, when we assign values, the setter method is automatically called.

p1 = Person('John', 36)

# Accessing attributes
print(p1.name)  # Output: 'John'
print(p1.age)   # Output: 36

# Modifying attributes
p1.name = 'Max'
print(p1.name)  # Output: 'Max'

# Assigning an invalid value
p1.age = 'Alice'  # Output: ValueError: Age must be an integer

Unlike before, assigning a value now actually triggers the setter method inside the class, enforcing our validation rules while still using a clean and simple dot notation.

Enforcing Validation at Initialization

Before wrapping up, let's look at what happens if we try to initialize an object with incorrect values, like an integer for name and a string for age. Right now, our class allows this:

p2 = Person(10, 'Alice')  # This will not raise an error

Since we were assigning values directly inside __init__, the setter methods were never triggered. However, we can fix this with a small change:

class Person:
    def __init__(self, name, age):
        self.name = name  # Calls the setter method
        self.age = age    # Calls the setter method

Instead of assigning values directly to _name and _age, we now use self.name = name and self.age = age. This change ensures that when an object is initialized, Python automatically calls the setter methods, applying the validation rules we defined earlier.

Now, if we try to initialize an object with invalid values, it will raise an error.

p2 = Person(10, 'Alice')  
# Output: ValueError: Name must be a string

Since self.name = name calls the name setter method, the validation inside set_name is triggered immediately, preventing invalid values from being assigned.

Python For Network Engineers - Introduction (I)
By the end of this course, you will be familiar with Python syntax, comfortable creating your own Python code, and able to configure/manage network devices as well as automate smaller manual tasks.
Written by
Suresh Vina
Tech enthusiast sharing Networking, Cloud & Automation insights. Join me in a welcoming space to learn & grow with simplicity and practicality.
Comments
More from Packetswitch
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Packetswitch.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.