Getters and Setters Interfaces via Property Decorators

🏷️ Object-Oriented Programming (OOP) Basics / Encapsulation

When building classes in Python, you often need to control how attributes are accessed and modified. Directly exposing attributes can lead to problemsβ€”someone might set an invalid value, or you might need to add validation logic later without breaking existing code. This is where getters and setters come in, and Python offers a clean, elegant way to implement them using property decorators.


🧠 What Are Getters and Setters?

Getters and setters are methods that control access to an object's attributes:

  • Getter – A method that retrieves the value of an attribute.
  • Setter – A method that sets or updates the value of an attribute, often with validation or transformation logic.

In many programming languages, you write explicit methods like get_attribute() and set_attribute(). Python simplifies this with the @property decorator, allowing you to define these methods while keeping the syntax as simple as attribute access.


βš™οΈ Why Use Getters and Setters?

  • Encapsulation – Hide internal implementation details from the outside world.
  • Validation – Ensure only valid data is assigned to an attribute.
  • Computed Attributes – Return values that are calculated on the fly, not stored directly.
  • Backward Compatibility – Add logic later without changing how the attribute is accessed in existing code.

πŸ› οΈ Basic Property Decorator Example

The @property decorator turns a method into a "getter". You can then define a @attribute_name.setter decorator for the setter.

Example without property decorator (manual getters/setters):

  • Define a class Temperature with a private attribute __celsius.
  • Create methods get_celsius() and set_celsius(value).
  • Call them explicitly: temp.get_celsius() and temp.set_celsius(25).

This works but feels clunky compared to natural attribute access.

Example with property decorator:

  • Define a class Temperature with a private attribute __celsius.
  • Use @property to define a getter method named celsius that returns self.__celsius.
  • Use @celsius.setter to define a setter method that validates the input before assigning.
  • Now you can write temp.celsius = 25 and print(temp.celsius) β€” clean and intuitive.

The setter can include logic like raising an error if the value is below absolute zero.


πŸ“Š Comparison: Direct Attribute vs. Property Decorator

Aspect Direct Attribute Access Property Decorator
Syntax obj.attribute = value obj.attribute = value (same!)
Validation None Can add validation in setter
Read-only Not possible Omit the setter to make it read-only
Computed values Not supported Getter can compute on the fly
Backward compatibility Breaking change if you add logic later Add logic without changing external code

πŸ•΅οΈ Read-Only Properties

If you only define a getter using @property and omit the setter, the attribute becomes read-only. Any attempt to assign a value will raise an AttributeError.

Example use case: A class Circle with a read-only property area that is calculated from the radius. Users can read circle.area but cannot set it directly.


πŸ”„ Computed Properties

Properties don't have to map directly to a stored attribute. They can compute and return values dynamically.

Example: A class Rectangle with attributes width and height. Define a property area that returns self.width * self.height. Every time you access rectangle.area, it recalculates based on current dimensions.


πŸ§ͺ Validation in Setters

One of the most powerful uses of property setters is input validation.

Example: A class Person with a property age. The setter checks: - Is the value an integer? If not, raise a TypeError. - Is the value between 0 and 150? If not, raise a ValueError.

This ensures the object never holds invalid data.


πŸ“ Best Practices

  • Use properties when you need to add logic to attribute access without changing the public interface.
  • Keep getters and setters simpleβ€”avoid heavy computation or side effects.
  • For truly simple attributes with no validation needed, just use a public attribute.
  • Use a leading underscore (_attribute) to indicate an attribute is "protected" or private by convention.
  • Use double underscore (__attribute) for name mangling in subclasses, but prefer single underscore for most cases.

🚩 Common Pitfalls

  • Infinite recursion – If your getter calls self.attribute instead of self._attribute, it will call itself endlessly. Always access the underlying private attribute directly inside getters and setters.
  • Overusing properties – Not every attribute needs a property. Use them only when you need control or computation.
  • Forgetting the setter – If you define a getter but later add a setter, make sure the setter name matches exactly: @property for attribute, then @attribute.setter.

βœ… Summary

Property decorators in Python provide a clean, Pythonic way to implement getters and setters. They let you:

  • Control access to attributes with validation.
  • Create read-only or computed attributes.
  • Maintain backward compatibility when adding logic later.
  • Keep your code readable and intuitive with natural attribute syntax.

By using @property and @attribute.setter, you embrace encapsulation without sacrificing simplicityβ€”a core strength of Python's design philosophy.


Property decorators let engineers control how attributes are accessed and modified while keeping the interface simple.


πŸ”§ Example 1: Basic property decorator for reading an attribute

This example shows how to use @property to make a method behave like a simple attribute.

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

temp = Temperature(25)
print(temp.celsius)

πŸ“€ Output: 25


πŸ”§ Example 2: Property with a setter to validate input

This example shows how to use @setter to add validation when an attribute is assigned.

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero")
        self._celsius = value

temp = Temperature(25)
temp.celsius = 30
print(temp.celsius)

πŸ“€ Output: 30


πŸ”§ Example 3: Property with a deleter to reset an attribute

This example shows how to use @deleter to define behavior when an attribute is deleted.

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero")
        self._celsius = value

    @celsius.deleter
    def celsius(self):
        self._celsius = 0

temp = Temperature(25)
del temp.celsius
print(temp.celsius)

πŸ“€ Output: 0


πŸ”§ Example 4: Computed property that transforms data

This example shows how a property can calculate a value from another attribute.

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

    @property
    def area(self):
        return self._width * self._height

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

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("Width must be positive")
        self._width = value

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

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("Height must be positive")
        self._height = value

rect = Rectangle(5, 10)
print(rect.area)
rect.width = 8
print(rect.area)

πŸ“€ Output: 50
πŸ“€ Output: 80


πŸ”§ Example 5: Property with caching for expensive calculations

This example shows how a property can store a computed result to avoid recalculating.

class DataProcessor:
    def __init__(self, data):
        self._data = data
        self._processed = None

    @property
    def processed_data(self):
        if self._processed is None:
            print("Processing data...")
            self._processed = [x * 2 for x in self._data]
        return self._processed

processor = DataProcessor([1, 2, 3, 4])
print(processor.processed_data)
print(processor.processed_data)

πŸ“€ Output: Processing data...
πŸ“€ Output: [2, 4, 6, 8]
πŸ“€ Output: [2, 4, 6, 8]


Comparison Table

Feature Regular Attribute Property Decorator
Syntax obj.attr obj.attr (same)
Validation No built-in Yes, via setter
Computed values No Yes, via getter
Delete behavior Default Custom, via deleter
Caching Manual Yes, with internal logic

When building classes in Python, you often need to control how attributes are accessed and modified. Directly exposing attributes can lead to problemsβ€”someone might set an invalid value, or you might need to add validation logic later without breaking existing code. This is where getters and setters come in, and Python offers a clean, elegant way to implement them using property decorators.


🧠 What Are Getters and Setters?

Getters and setters are methods that control access to an object's attributes:

  • Getter – A method that retrieves the value of an attribute.
  • Setter – A method that sets or updates the value of an attribute, often with validation or transformation logic.

In many programming languages, you write explicit methods like get_attribute() and set_attribute(). Python simplifies this with the @property decorator, allowing you to define these methods while keeping the syntax as simple as attribute access.


βš™οΈ Why Use Getters and Setters?

  • Encapsulation – Hide internal implementation details from the outside world.
  • Validation – Ensure only valid data is assigned to an attribute.
  • Computed Attributes – Return values that are calculated on the fly, not stored directly.
  • Backward Compatibility – Add logic later without changing how the attribute is accessed in existing code.

πŸ› οΈ Basic Property Decorator Example

The @property decorator turns a method into a "getter". You can then define a @attribute_name.setter decorator for the setter.

Example without property decorator (manual getters/setters):

  • Define a class Temperature with a private attribute __celsius.
  • Create methods get_celsius() and set_celsius(value).
  • Call them explicitly: temp.get_celsius() and temp.set_celsius(25).

This works but feels clunky compared to natural attribute access.

Example with property decorator:

  • Define a class Temperature with a private attribute __celsius.
  • Use @property to define a getter method named celsius that returns self.__celsius.
  • Use @celsius.setter to define a setter method that validates the input before assigning.
  • Now you can write temp.celsius = 25 and print(temp.celsius) β€” clean and intuitive.

The setter can include logic like raising an error if the value is below absolute zero.


πŸ“Š Comparison: Direct Attribute vs. Property Decorator

Aspect Direct Attribute Access Property Decorator
Syntax obj.attribute = value obj.attribute = value (same!)
Validation None Can add validation in setter
Read-only Not possible Omit the setter to make it read-only
Computed values Not supported Getter can compute on the fly
Backward compatibility Breaking change if you add logic later Add logic without changing external code

πŸ•΅οΈ Read-Only Properties

If you only define a getter using @property and omit the setter, the attribute becomes read-only. Any attempt to assign a value will raise an AttributeError.

Example use case: A class Circle with a read-only property area that is calculated from the radius. Users can read circle.area but cannot set it directly.


πŸ”„ Computed Properties

Properties don't have to map directly to a stored attribute. They can compute and return values dynamically.

Example: A class Rectangle with attributes width and height. Define a property area that returns self.width * self.height. Every time you access rectangle.area, it recalculates based on current dimensions.


πŸ§ͺ Validation in Setters

One of the most powerful uses of property setters is input validation.

Example: A class Person with a property age. The setter checks: - Is the value an integer? If not, raise a TypeError. - Is the value between 0 and 150? If not, raise a ValueError.

This ensures the object never holds invalid data.


πŸ“ Best Practices

  • Use properties when you need to add logic to attribute access without changing the public interface.
  • Keep getters and setters simpleβ€”avoid heavy computation or side effects.
  • For truly simple attributes with no validation needed, just use a public attribute.
  • Use a leading underscore (_attribute) to indicate an attribute is "protected" or private by convention.
  • Use double underscore (__attribute) for name mangling in subclasses, but prefer single underscore for most cases.

🚩 Common Pitfalls

  • Infinite recursion – If your getter calls self.attribute instead of self._attribute, it will call itself endlessly. Always access the underlying private attribute directly inside getters and setters.
  • Overusing properties – Not every attribute needs a property. Use them only when you need control or computation.
  • Forgetting the setter – If you define a getter but later add a setter, make sure the setter name matches exactly: @property for attribute, then @attribute.setter.

βœ… Summary

Property decorators in Python provide a clean, Pythonic way to implement getters and setters. They let you:

  • Control access to attributes with validation.
  • Create read-only or computed attributes.
  • Maintain backward compatibility when adding logic later.
  • Keep your code readable and intuitive with natural attribute syntax.

By using @property and @attribute.setter, you embrace encapsulation without sacrificing simplicityβ€”a core strength of Python's design philosophy.

Interactive Views

You are currently in πŸ“š All-in-One mode. Use the tabs at the top to switch to πŸ“– Theory Only or πŸ’» Code Only views.

Property decorators let engineers control how attributes are accessed and modified while keeping the interface simple.


πŸ”§ Example 1: Basic property decorator for reading an attribute

This example shows how to use @property to make a method behave like a simple attribute.

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

temp = Temperature(25)
print(temp.celsius)

πŸ“€ Output: 25


πŸ”§ Example 2: Property with a setter to validate input

This example shows how to use @setter to add validation when an attribute is assigned.

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero")
        self._celsius = value

temp = Temperature(25)
temp.celsius = 30
print(temp.celsius)

πŸ“€ Output: 30


πŸ”§ Example 3: Property with a deleter to reset an attribute

This example shows how to use @deleter to define behavior when an attribute is deleted.

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero")
        self._celsius = value

    @celsius.deleter
    def celsius(self):
        self._celsius = 0

temp = Temperature(25)
del temp.celsius
print(temp.celsius)

πŸ“€ Output: 0


πŸ”§ Example 4: Computed property that transforms data

This example shows how a property can calculate a value from another attribute.

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

    @property
    def area(self):
        return self._width * self._height

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

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("Width must be positive")
        self._width = value

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

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("Height must be positive")
        self._height = value

rect = Rectangle(5, 10)
print(rect.area)
rect.width = 8
print(rect.area)

πŸ“€ Output: 50
πŸ“€ Output: 80


πŸ”§ Example 5: Property with caching for expensive calculations

This example shows how a property can store a computed result to avoid recalculating.

class DataProcessor:
    def __init__(self, data):
        self._data = data
        self._processed = None

    @property
    def processed_data(self):
        if self._processed is None:
            print("Processing data...")
            self._processed = [x * 2 for x in self._data]
        return self._processed

processor = DataProcessor([1, 2, 3, 4])
print(processor.processed_data)
print(processor.processed_data)

πŸ“€ Output: Processing data...
πŸ“€ Output: [2, 4, 6, 8]
πŸ“€ Output: [2, 4, 6, 8]


Comparison Table

Feature Regular Attribute Property Decorator
Syntax obj.attr obj.attr (same)
Validation No built-in Yes, via setter
Computed values No Yes, via getter
Delete behavior Default Custom, via deleter
Caching Manual Yes, with internal logic