Web Analytics

Properties

Intermediate ~25 min read

The @property decorator transforms methods into attribute-like access, giving you the clean syntax of obj.value while keeping the control of getter/setter methods. This is Python's preferred way to implement managed attributes with validation, computation, or lazy loading.

Why Properties?

Properties let you:

  • Add validation without changing how users access the attribute
  • Create computed values that look like attributes
  • Make read-only attributes
  • Refactor internal implementation without breaking the public API

Property Basics

The @property decorator turns a method into a "getter" that's accessed like an attribute - no parentheses needed.

Output
Click Run to execute your code

Start Public, Add Properties Later

Python's philosophy: start with simple public attributes. If you later need validation or computation, convert to a property - the interface stays the same! Users still write obj.value, not obj.get_value().

Property Setters

Add a setter to allow assignment (obj.value = x) with validation. Use the @property_name.setter decorator.

Output
Click Run to execute your code

Setter Must Have the Same Name

The setter decorator must be @property_name.setter and the method name must match the property name. The internal storage should use a different name (typically with underscore: _property_name).

Computed Properties

Properties can compute values from other attributes - like area from width and height. These are typically read-only (no setter).

Output
Click Run to execute your code

@cached_property (Python 3.8+)

For expensive computations that don't change, use @cached_property from functools. It computes once on first access and caches the result. Unlike @property, it doesn't recompute on each access.

Practical Property Patterns

Real-world uses of properties: backward-compatible refactoring, dependent properties, lazy loading, and access tracking.

Output
Click Run to execute your code

Properties for API Stability

Properties let you change implementation details without changing the public interface. Users write the same obj.value whether it's stored directly, computed, validated, or lazily loaded.

Common Mistakes

1. Using the same name for property and storage

# Wrong - infinite recursion!
class Person:
    @property
    def name(self):
        return self.name  # Calls itself!

    @name.setter
    def name(self, value):
        self.name = value  # Calls itself!

# Correct - use different name for storage
class Person:
    @property
    def name(self):
        return self._name  # Underscore prefix

    @name.setter
    def name(self, value):
        self._name = value

2. Forgetting to use @property_name.setter

# Wrong - this creates a new property!
class Circle:
    @property
    def radius(self):
        return self._radius

    @property  # Wrong! Should be @radius.setter
    def radius(self, value):
        self._radius = value

# Correct
class Circle:
    @property
    def radius(self):
        return self._radius

    @radius.setter  # Correct decorator
    def radius(self, value):
        self._radius = value

3. Not using the setter in __init__

# Problem - validation bypassed in __init__
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius  # Bypasses validation!

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

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._celsius = value

# Better - use property in __init__ for validation
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius  # Uses setter - validates!

4. Making properties do too much work

# Bad - expensive operation on every access
class Report:
    @property
    def data(self):
        # This runs a database query EVERY time!
        return self.database.fetch_all_records()

# Better - cache expensive operations
class Report:
    @property
    def data(self):
        if self._data is None:
            self._data = self.database.fetch_all_records()
        return self._data

# Or use @cached_property (Python 3.8+)

5. Properties with side effects

# Bad - getter with surprising side effects
class Counter:
    @property
    def value(self):
        self._access_count += 1  # Side effect!
        return self._value

# Getters should be "pure" - no unexpected changes
# If you need to track access, be explicit about it

Exercise: Temperature Converter

Task: Create a Temperature class with properties for multiple units.

Requirements:

  • celsius property with getter and setter (validates >= -273.15)
  • fahrenheit property with getter and setter (converts to/from celsius)
  • kelvin property - read-only getter (celsius + 273.15)
  • __str__ showing all three units
Output
Click Run to execute your code
Show Solution
class Temperature:
    def __init__(self, celsius=0):
        self.celsius = celsius  # Uses setter

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

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero!")
        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

    @property
    def kelvin(self):
        return self._celsius + 273.15
    # No kelvin setter - read-only!

    def __str__(self):
        return f"{self.celsius}°C / {self.fahrenheit}°F / {self.kelvin}K"


# Test
t = Temperature(0)
print(t)  # 0°C / 32.0°F / 273.15K

t.celsius = 100
print(t)  # 100°C / 212.0°F / 373.15K

t.fahrenheit = 68
print(t)  # 20.0°C / 68.0°F / 293.15K

Summary

  • @property creates a getter - access method like an attribute
  • @name.setter creates a setter with validation
  • @name.deleter handles del obj.name
  • Store internal value with underscore: _name (not same as property name!)
  • Computed properties derive values from other attributes
  • Use @cached_property for expensive one-time computations

What's Next?

Now that you understand properties, learn about Abstract Classes - using the abc module to define interfaces that subclasses must implement, ensuring consistent APIs across related classes.