Property Decorator
Overview
The `@property` decorator transforms a class method so it can be accessed like a normal attribute, evaluating its logic on-the-fly. If you have a `User` object, you can compute `user.full_name` dynamically rather than storing it. You can also define `@property_name.setter` and `@property_name.deleter` methods under the same property name to intercept assignment or deletion. class User: def __init__(self, first_name, last_name): self.first = first_name self.last = last_name @property def full_name(self): return f"{self.first} {self.last}" @full_name.setter def full_name(self, name): self.first, self.last = name.split() The mental model: variables are usually direct labels to objects, but a property is a "computed label" that secretly runs a function whenever you read, write, or delete it. Analogy: a property is like an automatic sliding door—it looks like a regular wall (an attribute), but approaching it triggers a hidden motor (a function call). Properties are designed to work on class instances, not the class itself. Accessing a property directly on the class returns the property object rather than evaluating the method. class Settings: @property def timeout(self): return 30 # Returns <property object at 0x...>, not 30! print(Settings.timeout)
It allows you to safely change internal implementation without breaking the external API. You can start with a simple attribute, and later add validation or computation without forcing everyone to rewrite `obj.attr` to `obj.get_attr()`.
Where used: `SQLAlchemy` models, `Pydantic` computed fields, `Django` models
Why learn this
- You can add validation to class attributes without changing the public API.
- It makes objects cleaner to use, avoiding Java-style `get_foo()` and `set_foo()` sprawl.
Code walkthrough
class User:
def __init__(self, age):
self._age = age
@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
u = User(20)
u.age = 25
print(u.age)
Focus: u.age = 25
Aha moment
class BankAccount:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, amount):
self._balance = amount
acc = BankAccount(100)
acc.balance += 50
print(acc.balance)
Prediction: What does this print?
Common guess: It raises an error because you can't use += on a property.
The `+=` operator automatically expands to `acc.balance = acc.balance + 50`, effectively calling both the getter to read the current balance and the setter to write the new balance.
Common mistakes
- Infinite Recursion in Setters: If a setter for `@foo.setter` modifies `self.foo = value`, it triggers the setter again! You must modify a private underlying attribute like `self._foo = value`.
- Missing the @property on Getters: You cannot define `@foo.setter` unless `foo` is already defined as a `@property` method first.
Glossary
- evaluating
- Running a piece of code or calculation to determine its final result or value. Example: `result = 2 + 2`.
- sprawl
- When code becomes messy, spread out, and difficult to manage because too many similar or repetitive methods are used. Example: `obj.get_name()`.
Recall questions
- Why would you use `@property` instead of a regular method like `get_full_name()`?
- What happens if you try to assign a value to a property that only has a getter defined?
- What is the correct mental model for how a property functions when accessed?
- What is the result of accessing a `@property` directly on the class rather than an instance?
Understanding checks
What happens when you run this code?
It raises an `AttributeError`.
The `env` property only has a getter defined. Attempting to assign to a property without a corresponding `@env.setter` raises an `AttributeError`.
Why would you choose to use a `@property` instead of leaving `age` as a simple public integer attribute on a `User` class?
To allow future validation or computation without breaking the external API.
Using a property lets external code keep using `user.age`, but allows you to add a hidden function to intercept assignments (e.g., preventing negative values) without forcing everyone to change to `user.set_age(...)`.
Practice tasks
Implement a Read-Only Computed Property
Modify the starter code for the `Rectangle` class to add an `@property` called `area` that computes and returns `self.width * self.height` on-the-fly. Do not provide a setter.
Challenge
Validating Password Length
Create a `UserAccount` class that initializes with a `username` and a `password`. Use a property for `password` to ensure it is always stored in `self._password`. The setter should raise a `ValueError` with the message 'Password too short' if the new password is less than 8 characters long. The constructor should also validate the initial password using this setter.
Continue learning
Previous: Defining functions and return values
Previous: Function Decorators