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.