Python notes

property

Property

  • A property is an attribute whose value is computed dynamically at access time.
  • property enables dynamic behavior for what appears to be a static attribute.

Problem with Static Attributes

class Person:
    def __init__(self, first, last):
        self.first = first
        self.last = last
        self.name = f"{self.first} {self.last}"
  • Changing first or last won't update name, causing inconsistency.

Non-Pythonic Fixes

  1. Manual setters

    def set_first(self, first): ...
    def set_last(self, last): ...
  2. Method-based getter

    def get_name(self): return ...

Pythonic Solution: Using @property

@property
def name(self):
    return f"{self.first} {self.last}"
  • Dynamically computes the name when accessed.
  • Keeps value always up to date with first and last.

Common Use Case: Dynamic Attribute Calculation

  • Example: Person.age depending on birthdate and current date
@property
def age(self):
    today = dt.date.today()
    will_celebrate = self.birthdate.replace(year=today.year) > today
    return today.year - self.birthdate.year - will_celebrate

  • Example 2: Property for RGB to Hex Conversion
class Colour:
    def __init__(self, r, g, b):
        self.r = r
        self.g = g
        self.b = b

    @property
    def hex(self):
        return f"#{self.r:02x}{self.g:02x}{self.b:02x}"

Usage:

>>> c = Colour(146, 255, 0)
>>> c.hex
'#92ff00'

Read-only Attributes with Properties

@property
def first(self):
    return self._first
  • Prevents setting: john.first = "..." → AttributeError
  • Underscore-prefixed attributes can still be changed manually
# Standard Library Example: `fractions.Fraction

@property
def numerator(a):
    return a._numerator

Adding a Setter with @<prop>.setter

@name.setter
def name(self, full_name):
    self.first, self.last = full_name.split()

Usage:

john.name = "Charles Smith"

Example: Colour.hex**

@hex.setter
def hex(self, hexstr):
    hexstr = hexstr.lstrip("#")
    self.r, self.g, self.b = tuple(int(hexstr[i:i+2], 16) for i in (0, 2, 4))
>>> c.hex = "#ff00ff"
>>> c.r, c.g, c.b
(255, 0, 255)

When You See property 'x' of 'Y' object has no setter

  • Indicates a property is read-only
  • Or a setter was not defined

Property Deleters with @<prop>.deleter

@first.deleter
def first(self):
    del self._first

Usage:

del john.first
  • Deleting the property does not remove the property itself—only internal data.
# Standard Library Example: `Request.full_url`

@full_url.deleter
def full_url(self):
    self._full_url = None
    self.fragment = None
    self.selector = ''

Using property() Directly (Without Decorators)

x = property(getx, setx, delx, "Docstring")
  • Less common, but useful if method access is also desired
  • Example from calendar.Calendar:
    firstweekday = property(getfirstweekday, setfirstweekday)

But why not just function call?

✅ Properties make your object’s interface feel more like working with data, not with machinery.

It’s not just about () vs not — it’s about design intention, readability, and future flexibility.

🔍 So when should you use a method vs a property?

Use a method when... Use a property when...
It’s expensive or has side effects It’s lightweight, fast, and safe to compute
It requires parameters It’s based only on internal state
The action is more like a command The result feels like an attribute (e.g., .age)
You want to make it clear: "this is a process" You want to make it look like data on the object

🧠 Think like an API designer

Let’s say you have:

person.age()

It works, but what’s the first impression?

This feels like it does something non-trivial or maybe needs arguments.

Now:

person.age

Now it’s clear:

“Oh! This is just some attribute about the person.”

Even if it's calculated, it feels like part of their state, not an action you command.


🔧 Real-World Flexibility: Migration Path

Let’s say today you do this:

class Product:
    def __init__(self, price):
        self.price = price
        self.tax_rate = 0.1

    def total_price(self):
        return self.price * (1 + self.tax_rate)

All your code has p.total_price() everywhere.

Tomorrow, you decide to make it a property:

@property
def total_price(self):
    ...

Now you have to update every call to drop the ().
But if you start with a property, and later need to refactor to use caching or more logic inside — the outside world doesn’t need to change a thing.


So in summary:

  • using @property is about cleaner, intention-driven design, and lets you hide implementation details while keeping the interface consistent.