# 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:

```python
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:"

```python
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:

```python
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:**

```bash
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:**

```bash
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:

```bash
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:"

```python
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.

```python
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:

```python
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:**

```bash
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:**

```bash
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:

```bash
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:

```python
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](https://www.pacificw.com) - 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*](https://www.tech-reader.blog) *and the author of* [*Think Like a Genius*](https://amazon.com/author/aaron.rose)*.*
