Python by Structure: Property Decorators and Managed Attributes

Timothy was adding a Rectangle class to a geometry library when he ran into a design problem. “Margaret, I need an area attribute on my rectangles, but the area depends on width and height. Should I calculate it in __init__ and store it, or make users call an area() method every time?”

Margaret looked at his dilemma. “Neither. You want the area to look like an attribute but calculate itself automatically. That’s what the @property decorator does.”

“Wait,” Timothy said. “I thought decorators were for wrapping functions or transforming classes. How does a decorator make something look like an attribute?”

The Problem: Methods or Attributes?

Margaret showed him the traditional approach:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height  # Stored attribute

“This works,” Margaret said, “but what happens if someone changes the width?”

Timothy thought about it. “The stored area becomes wrong. It doesn’t recalculate.”

“Right. So some people use a method instead:”

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

    def area(self):
        return self.width * self.height

“Now users have to call rect.area() with parentheses, which feels clunky for something that should just be a value.”

The Solution: @property

Margaret showed him the property decorator approach:

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

    @property
    def area(self):
        """The area of the rectangle."""
        return self._width * self._height

# Usage
rect = Rectangle(4, 5)
print(f"Width: {rect._width}")
print(f"Area: {rect.area}")  # No parentheses!

Timothy noticed the naming change. “You added underscores to _width and _height. Why?”

“Good catch,” Margaret said. “The leading underscore is a Python convention that signals ‘internal use.’ It tells other developers these are implementation details that shouldn’t be accessed directly. Users should interact with our managed properties instead.”

Timothy stared at the code. “You decorated a method called area, but then you access it without parentheses? How does that work?”

Understanding @property

Margaret showed him the structure:

Tree View:

class Rectangle
    __init__(self, width, height)
        self._width = width
        self._height = height
    @property
    area(self)
        'The area of the rectangle.'
        Return self._width * self._height

rect = Rectangle(4, 5)
print(f'Width: {rect._width}')
print(f'Area: {rect.area}')

English View:

Class Rectangle:
  Function __init__(self, width, height):
    Set self._width to width.
    Set self._height to height.
  Decorator @property
  Function area(self):
    Evaluate 'The area of the rectangle.'.
    Return self._width * self._height.

Set rect to Rectangle(4, 5).
Evaluate print(f'Width: {rect._width}').
Evaluate print(f'Area: {rect.area}').

“Look at the structure,” Margaret said. “The @property decorator transforms the area method into a property descriptor. When you access rect.area without parentheses, Python intercepts that access and calls your method automatically.”

Timothy traced through it. “So when I write rect.area, Python sees that area is a property, not a regular attribute, and calls the method I defined?”

“Exactly. The method becomes a getter. From the outside, it looks like a simple attribute access, but Python runs your calculation code every time.”

The output confirmed it:

Width: 4
Area: 20

“So the area is always correct, even if I change the width, because it recalculates on every access?”

“Precisely. Try this:”

rect._width = 10
print(f"New area: {rect.area}")  # Outputs: 100

Timothy’s eyes lit up. “It recalculated! The area updates automatically when the dimensions change.”

“But watch what happens if you try to set the area directly,” Margaret said.

rect.area = 50  # AttributeError: can't set attribute

Timothy frowned at the error. “It won’t let me assign to it?”

“Right. Properties are read-only by default. The @property decorator only creates a getter. If you want users to be able to set the value, you need to add a setter explicitly.”

Adding Setters

“How do I add a setter?” Timothy asked.

Margaret showed him:

class Temperature:
    def __init__(self):
        self._celsius = 0

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

    @celsius.setter
    def celsius(self, value):
        self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9

# Usage
temp = Temperature()
temp.celsius = 25
print(f"Celsius: {temp.celsius}, Fahrenheit: {temp.fahrenheit}")

temp.fahrenheit = 77
print(f"Celsius: {temp.celsius}, Fahrenheit: {temp.fahrenheit}")

Tree View:

class Temperature
    __init__(self)
        self._celsius = 0
    @property
    celsius(self)
        Return self._celsius
    @celsius.setter
    celsius(self, value)
        self._celsius = value
    @property
    fahrenheit(self)
        Return self._celsius * 9/5 + 32
    @fahrenheit.setter
    fahrenheit(self, value)
        self._celsius = (value - 32) * 5/9

temp = Temperature()
temp.celsius = 25
print(f'Celsius: {temp.celsius}, Fahrenheit: {temp.fahrenheit}')
temp.fahrenheit = 77
print(f'Celsius: {temp.celsius}, Fahrenheit: {temp.fahrenheit}')

English View:

Class Temperature:
  Function __init__(self):
    Set self._celsius to 0.
  Decorator @property
  Function celsius(self):
    Return self._celsius.
  Decorator @celsius.setter
  Function celsius(self, value):
    Set self._celsius to value.
  Decorator @property
  Function fahrenheit(self):
    Return self._celsius * 9/5 + 32.
  Decorator @fahrenheit.setter
  Function fahrenheit(self, value):
    Set self._celsius to (value - 32) * 5/9.

Set temp to Temperature().
Set temp.celsius to 25.
Evaluate print(f'Celsius: {temp.celsius}, Fahrenheit: {temp.fahrenheit}').
Set temp.fahrenheit to 77.
Evaluate print(f'Celsius: {temp.celsius}, Fahrenheit: {temp.fahrenheit}').

Timothy studied the structure carefully. “So you first define the property with @property to create the getter, then you use @celsius.setter to add a setter to that same property?”

“Exactly. The @property decorator creates the property object and assigns it to celsius. Then @celsius.setter modifies that property object to add setter behavior.”

The output showed the conversion working:

Celsius: 25, Fahrenheit: 77.0
Celsius: 25.0, Fahrenheit: 77.0

“Wait,” Timothy said. “When I set fahrenheit = 77, it converted that to celsius and stored it in _celsius. Then when I read celsius, it gave me that converted value?”

“Right. Both properties read from and write to the same underlying _celsius attribute, but they convert the values. The setter intercepts the assignment and runs your conversion code.”

When to Use Properties

Timothy was starting to see the pattern. “So properties are for computed values, validation, or conversion?”

Margaret listed the common use cases:

“Use @property when you want to:

  • Calculate values on-the-fly instead of storing them
  • Validate inputs before storing them
  • Convert between different representations of the same data
  • Control access to internal attributes
  • Maintain backwards compatibility when refactoring”

She showed him a validation example:

class Person:
    def __init__(self, name):
        self._name = name
        self._age = 0

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value

“Now any attempt to set a negative age raises an error, but from the outside it looks like a simple attribute.”

Timothy nodded. “So properties give me the clean syntax of attributes with the power of methods – calculation, validation, conversion, all hidden behind simple assignment and access.”

“That’s exactly it,” Margaret said. “The structure shows that @property and @name.setter are decorators that transform methods into managed attributes. The user sees simple attribute access, but you control what happens behind the scenes.”

Analyze Python structure yourself: Download the Python Structure Viewer – a free tool that shows code structure in tree and plain English views. Works offline, no installation required.

Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Similar Posts