Menu
Coddy logo textTech

Property Decorators Advanced

Part of the Object Oriented Programming section of Coddy's Python journey — lesson 31 of 64.

Advanced property decorators provide more sophisticated control over attribute access, including computed properties, deleters, and full property management.

Here is an example of computed properties that derive values from other attributes:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        return self.width * self.height
    
    @property
    def perimeter(self):
        return 2 * (self.width + self.height)

Use computed properties like regular attributes:

rect = Rectangle(5, 3)
print(rect.area)      # 15 - calculated automatically
print(rect.perimeter) # 16 - calculated automatically

Create a property with getter, setter, and deleter:

class Temperature:
    def __init__(self):
        self._temp = 0
    
    @property
    def temperature(self):
        return self._temp
    
    @temperature.setter
    def temperature(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero!")
        self._temp = value
    
    @temperature.deleter
    def temperature(self):
        print("Resetting temperature to 0")
        self._temp = 0

Use the full property functionality:

temp = Temperature()

Use the setter with validation:

temp.temperature = 25
print(temp.temperature)  # 25

# temp.temperature = -300  # Would raise ValueError

Use the deleter:

del temp.temperature
print(temp.temperature)  # 0

Create a more complex example with a game score:

class Player:
    def __init__(self, name):
        self.name = name
        self._score = 0
        self._level = 1
    
    @property
    def score(self):
        return self._score
    
    @score.setter
    def score(self, value):
        if value >= 0:
            self._score = value
            self._level = (value // 1000) + 1
        else:
            raise ValueError("Score cannot be negative")
    
    @score.deleter
    def score(self):
        print(f"Resetting {self.name}'s progress")
        self._score = 0
        self._level = 1
    
    @property
    def level(self):
        return self._level

player = Player("Alice")
player.score = 2500
print(f"Score: {player.score}, Level: {player.level}")  # Score: 2500, Level: 3

del player.score
print(f"Score: {player.score}, Level: {player.level}")  # Score: 0, Level: 1

Output:

15
16
25
Resetting temperature to 0
0
Score: 2500, Level: 3
Resetting Alice's progress
Score: 0, Level: 1

Key Point: Advanced property decorators allow computed properties (calculated from other data), property deletion with @property.deleter, and full control over getting, setting, and deleting attributes. This creates intuitive interfaces while maintaining strong data validation and encapsulation.

challenge icon

Challenge

Medium

In this challenge, you'll implement a Rectangle class with proper encapsulation and property validation.

  • rectangle.py - This is the file you need to edit, containing TODO comments to guide your implementation
  • driver.py - Contains extensive test scenarios (do not modify)
  1. Implement private attributes for _width and _height
  2. Create properties for width and height with validation (must be positive)
    • Raise appropriate ValueError messages as specified in the TODOs
    • Important: Check if the value is less than or equal to 0, then raise the error (this ensures consistent behavior)
  3. Implement read-only properties for area and perimeter
  4. Create a dimensions property with getter, setter, and deleter functionality as described in the TODOs
    • Important: In the setter, use tuple unpacking syntax: width, height = dimensions

Cheat sheet

Advanced property decorators provide sophisticated control over attribute access with computed properties, deleters, and full property management.

Computed Properties:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        return self.width * self.height
    
    @property
    def perimeter(self):
        return 2 * (self.width + self.height)

Full Property with Getter, Setter, and Deleter:

class Temperature:
    def __init__(self):
        self._temp = 0
    
    @property
    def temperature(self):
        return self._temp
    
    @temperature.setter
    def temperature(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero!")
        self._temp = value
    
    @temperature.deleter
    def temperature(self):
        print("Resetting temperature to 0")
        self._temp = 0

Usage:

# Computed properties
rect = Rectangle(5, 3)
print(rect.area)      # 15
print(rect.perimeter) # 16

# Full property functionality
temp = Temperature()
temp.temperature = 25  # Uses setter
print(temp.temperature)  # Uses getter

del temp.temperature   # Uses deleter

Try it yourself

from rectangle import Rectangle

# Test case handler
test_case = input()

# Basic functionality test
if test_case == "default_test":
    rect = Rectangle(5, 3)
    print(f"Width: {rect.width}, Height: {rect.height}")
    print(f"Area: {rect.area}, Perimeter: {rect.perimeter}")
    
    # Test the dimensions property
    print(f"Dimensions: {rect.dimensions}")
    rect.dimensions = (10, 8)
    print(f"New area: {rect.area}")
    
    # Test validation
    try:
        rect.width = -2
    except ValueError as e:
        print(f"Validation error: {e}")
    
    # Test deleter
    del rect.dimensions
    print(f"After reset: {rect.dimensions}")

# Test with zero values
elif test_case == "zero_values":
    try:
        rect = Rectangle(0, 5)
    except ValueError as e:
        print(f"Error creating rectangle: {e}")
    
    try:
        rect = Rectangle(5, 0)
    except ValueError as e:
        print(f"Error creating rectangle: {e}")

# Test with negative values
elif test_case == "negative_values":
    rect = Rectangle(5, 3)
    original_dimensions = rect.dimensions
    
    try:
        rect.dimensions = (5, -3)
    except ValueError as e:
        print(f"Error setting dimensions: {e}")
    
    print(f"Dimensions after failed update: {rect.dimensions}")
    print(f"Original dimensions preserved: {rect.dimensions == original_dimensions}")

# Test with large values
elif test_case == "large_values":
    rect = Rectangle(1000000, 2000000)
    print(f"Large rectangle area: {rect.area}")
    print(f"Large rectangle perimeter: {rect.perimeter}")

# Test with floating point values
elif test_case == "float_values":
    rect = Rectangle(3.5, 2.75)
    print(f"Dimensions: {rect.dimensions}")
    print(f"Area: {rect.area}")
    print(f"Perimeter: {rect.perimeter}")
    
    rect.dimensions = (1.1, 2.2)
    print(f"New area with float dimensions: {rect.area}")

# Test with type errors
elif test_case == "type_errors":
    try:
        rect = Rectangle("5", 3)
    except Exception as e:
        print(f"Type error during creation: {type(e).__name__}: {e}")
    
    rect = Rectangle(5, 3)
    try:
        rect.dimensions = 10  # Not a tuple
    except Exception as e:
        print(f"Type error setting dimensions: {type(e).__name__}: {e}")

# Test multiple property operations
elif test_case == "property_operations":
    rect = Rectangle(5, 10)
    print(f"Initial - Width: {rect.width}, Height: {rect.height}, Area: {rect.area}")
    
    rect.width = 8
    print(f"After width change - Width: {rect.width}, Area: {rect.area}")
    
    rect.height = 6
    print(f"After height change - Height: {rect.height}, Area: {rect.area}")
    
    rect.dimensions = (12, 9)
    print(f"After dimensions change - Dimensions: {rect.dimensions}, Area: {rect.area}")
    
    del rect.dimensions
    print(f"After reset - Dimensions: {rect.dimensions}, Area: {rect.area}")

# Test property validation edge cases
elif test_case == "validation_edge_cases":
    rect = Rectangle(5, 3)
    
    try:
        rect.width = 0
    except ValueError as e:
        print(f"Zero width error: {e}")
    
    # Very small positive value should be accepted
    rect.height = 0.0001
    print(f"Small height accepted: {rect.height}")
    print(f"Area with small height: {rect.area}")

# Test multiple rectangles
elif test_case == "multiple_rectangles":
    rect1 = Rectangle(5, 3)
    rect2 = Rectangle(10, 2)
    rect3 = Rectangle(4, 4)
    
    print(f"Rectangle 1 - Area: {rect1.area}, Perimeter: {rect1.perimeter}")
    print(f"Rectangle 2 - Area: {rect2.area}, Perimeter: {rect2.perimeter}")
    print(f"Rectangle 3 - Area: {rect3.area}, Perimeter: {rect3.perimeter}")
    
    # Modify each rectangle
    rect1.width = 7
    rect2.height = 5
    rect3.dimensions = (6, 6)
    
    print(f"After modifications:")
    print(f"Rectangle 1 - Dimensions: {rect1.dimensions}")
    print(f"Rectangle 2 - Dimensions: {rect2.dimensions}")
    print(f"Rectangle 3 - Dimensions: {rect3.dimensions}")

# Test performance with many operations
elif test_case == "performance_test":
    rect = Rectangle(5, 5)
    
    # Perform many property operations
    for i in range(1000):
        rect.width = i % 10 + 1  # Values 1-10
        rect.height = i % 5 + 1  # Values 1-5
        area = rect.area  # Access computed property
        perimeter = rect.perimeter  # Access computed property
        dims = rect.dimensions  # Access property getter
    
    print(f"Final state after 1000 operations:")
    print(f"Width: {rect.width}, Height: {rect.height}")
    print(f"Area: {rect.area}, Perimeter: {rect.perimeter}")
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming