Python

P
PythonHub
Progress 0%

Python

  • Home
  • History of Python
  • Applications of Python
  • Introduction To Python
    • What is Python
    • Character Set
    • Tokens in Python
    • Python Execution Mode
    • Variable And Identifiers
    • Data Types in Python
    • Operators And Expressions
    • Constants in Python
    • Assignment Statement
    • Input / Output in Python
    • Simple Python Scripts
    • Namespace in Python
    • šŸ“ Assignments
  • Operators in Python
    • Arithmetic Operators
    • Assignment Operators
    • Shorthand Assignment Operators
    • Relational Operators
    • Logical Operators
    • Bitwise Operators
    • Special Operators
    • šŸ“ Assignments
  • Input Output in Python
    • Accept Input
    • Output Formatting
    • šŸ“ Assignments
  • Conditional Statement
    • Decision Making
    • if Statement
    • IF-ELSE STATEMENT
    • IF-ELSE LADDER
    • NESTED IF-ELSE
    • Short Hand IF-ELSE
    • šŸ“ Assignments
  • Loops
    • Introduction to Loops
    • While Loop
    • Nested While Loop
    • šŸ“ While Loop Assignments
    • For Loop
    • For Loop Examples
    • Nested For Loop
    • Nested For Loop Examples
    • Infinite While Loops
    • Infinite For Loops
    • Break, Continue and Else in Loops
    • Difference Between For and While Loop
    • For Each Loop
    • šŸ“ For Each Assignments
    • šŸ“ All Loops Assignments
  • List
    • List in Python
    • Access List Elements
    • List Functions
    • Iterate (Loop) List
    • List Comprehension
    • šŸ“ Assignments
  • Tuple
    • Tuple in Python
    • Access Tuple Elements
    • Tuple Functions
    • Iterate (Loop) Tuple
    • Unpack Tuple
    • Tuple Comprehension
    • šŸ“ Assignments
  • Set
    • Set in Python
    • Access Set Elements
    • Set Methods
    • Iterate (Loop) Set
    • Pack/Unpack Set
    • Set Comprehension
    • šŸ“ Assignments
  • Dictionary
    • Dictionary
    • Access Dictionary Items
    • Dictionary Methods
    • Iterate (Loop) Dictionary
    • Formatting Dictionaries
    • Nested Dictionaries
    • Dictionary Comprehension
    • šŸ“ Assignments
  • Diff List Tuple Set Dictionary
    • List vs Tuple
    • List vs Set
    • List vs Dictionary
    • Tuple vs Set
    • Tuple vs Dictionary
    • Dictionary vs Set
    • šŸ“ Assignments
  • Exception
    • Error vs Exception
    • Types of Exception
    • Exception Handling
    • User Defined Exception
    • Logging Exception
    • šŸ“ Assignments
  • Functions
    • Introduction to Functions
    • Modular Programming
    • Types of Functions
    • Inbuilt Functions
    • Need For User-Defined Function
    • Elements of User Defined Function
    • Function Arguments
    • Nesting of Functions
    • Recursion
    • Global Local and Non Local
    • Python Lambda Functions
    • šŸ“ Assignments
  • Python Module
    • Introduction to Module
    • Inbuilt Modules in Python
    • User Defined Module
    • šŸ“ Assignments
  • File Handling
    • Introduction to Files
    • Create File
    • Read Files
    • Write to File
    • Rename File
    • Copy File
    • Move File
    • List Files in Directory
    • Binary Files
    • Zipping and Unzipping Files
    • šŸ“ Assignments
  • Strings
    • Basics of Strings
    • String Special Operators
    • String Formatting Operators
    • String Methods
    • šŸ“ Assignments
  • Regular Expressions
  • Python OOPS
    • Basics of Object Oriented
    • What are Classes and Objects?
    • Creating Class and Object
    • OOP vs Procedural Programming
    • Difference Between Classes and Objects
    • Constructors
    • Destructor
    • Built Class Methods and Attributes
    • Class and Instance Variables
    • Inheritance in Python
    • Single Inheritance
    • Multiple Inheritance
    • Multilevel Inheritance
    • Hierarchical Inheritance
    • Hybrid Inheritance
    • Abstraction
    • Method Overriding
    • Abstract Method
    • Interfaces in Python
    • Abstract Class vs Interface
    • Public, Private and Protected
    • Overloading vs Overriding
    • Inheritance vs Composition
    • Encapsulation
    • Polymorphism
    • Inner Classes
    • šŸ“ Assignments
  • Advanced Python
    • Iterator in Python
    • Generator in Python
    • Decorator in Python
    • Type Hints in Python
    • Async/Await Programming
    • Dataclasses in Python
    • Context Managers in Python
    • Property Decorator in Python
    • __slots__ in Python
    • Enums in Python
    • Walrus Operator in Python
    • Match-Case in Python
    • šŸ“ Assignments
  • Python Standard Library
    • Collections Module
    • Itertools Module
    • Functools Module
    • Datetime Module
    • JSON Module
    • OS Module
    • Sys Module
    • Random Module
    • Math Module
    • šŸ“ Assignments
  • Python Testing
    • Unit Testing in Python
    • Pytest Framework
    • Mocking in Python
    • šŸ“ Assignments
  • Python Best Practices
    • PEP 8 Style Guide
    • Docstrings in Python
    • Logging in Python
    • Code Optimization Tips
    • Debugging Techniques
    • šŸ“ Assignments
  • MySQL Database in Python
    • Introduction to MySQL with Python
    • DBMS vs File System
    • Connecting to MySQL Database
    • Create Database in MySQL
    • Create Table in MySQL
    • Insert Data in MySQL
    • Insert Multiple Rows
    • Select Data from MySQL
    • WHERE Clause in MySQL
    • Update Data in MySQL
    • Delete Data from MySQL
    • Parameterized Queries
    • Transaction Management
    • Error Handling
    • Connection Pooling
    • MySQL Drivers Guide
    • Joins in MySQL
    • Aggregation Functions
    • Backup MySQL Database
    • Best Practices
    • šŸ“ Assignments
  • MySQL Database Operations
    • SELECT Statement
    • MySQL Operators
    • DDL Statements
    • DML Statements
    • Subqueries
    • JOIN Operations
    • Aggregation
    • Case Study
    • šŸ“ Assignments
  • Graphics in Python
  • Threads in Python
    • Introduction to Threads
    • Process vs Threads
    • Concurrent Programming & GIL
    • Uses of Threads
    • Creating Threads
    • Single Tasking
    • Multi Tasking
    • Thread Synchronization
    • šŸ“ Assignments
  • Interview Questions & Answers
  • Python Case Studies
  • Multiple Choice Questions
  • šŸ“ Practice Problems
Get in Touch
  • tech2dsm@gmail.com

© Sankalan Data Tech

Python Language Interactive Tutorial

Python: Property Decorator

Python Property Decorator - Complete Guide

Learn how to control attribute access with @property.

Created by Sankalan Data Tech Team Verified
Data Engineers, Analysts, Scientists & Trainers
Created by experienced Python developers, data engineers, and data scientists to make programming easy through practical examples, real-world experience, and clear explanations.
On this page:
  • What is the @property Decorator?
  • Why Use @property?
  • The Getter - Reading Values
  • The Setter - Setting Values
  • The Deleter - Deleting Values
  • Computed Properties
  • Real-World Example
  • Best Practices
  • Try It Yourself
  • Quick Quiz
  • Frequently Asked Questions
Share this tutorial:
Twitter LinkedIn Facebook WhatsApp Reddit Telegram Email Copy Link
What You'll Learn Here
  • What is @property — a decorator that makes methods look like attributes
  • Why use it — cleaner code, validation, computed values
  • Getter — how to read values
  • Setter — how to set values with validation
  • Deleter — how to delete values
  • Computed properties — values calculated on the fly

What is the @property Decorator?

The @property decorator is a built-in Python decorator that lets you define methods that can be accessed like attributes. It's a clean and Pythonic way to implement getters, setters, and deleters.

Think of @property like a receptionist. When you want to talk to someone in a company (access an attribute), you go through the receptionist. The receptionist controls who you can talk to, checks your ID (validation), and can even transfer you to someone else (computed values).

Instead of calling obj.get_value(), you can just use obj.value. And instead of obj.set_value(10), you use obj.value = 10. Much cleaner!

šŸ’” Key concept: @property lets you add logic to attribute access while keeping the simple attribute syntax.

Why Use @property?

1

The Benefits of @property

Let's see why @property makes your code better.

# Why Use @property?

print("=" * 50)
print("WHY USE @property?")
print("=" * 50)

# ============================================================
# WITHOUT @property — Getters and Setters (Verbose)
# ============================================================

print("\nāŒ WITHOUT @property:")

class PersonOld:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    # Getter method
    def get_name(self):
        return self._name
    
    # Setter method
    def set_name(self, name):
        if not name:
            raise ValueError("Name cannot be empty")
        self._name = name
    
    # Getter method
    def get_age(self):
        return self._age
    
    # Setter method
    def set_age(self, age):
        if age < 0:
            raise ValueError("Age cannot be negative")
        self._age = age

person = PersonOld("Alice", 30)
print(f"   Name: {person.get_name()}")  # Have to call methods
print(f"   Age: {person.get_age()}")
person.set_name("Bob")
person.set_age(25)

print("   āŒ Too many method calls for simple attribute access")


# ============================================================
# WITH @property — Clean and Simple
# ============================================================

print("\nāœ… WITH @property:")

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    @property
    def name(self):
        """Get the name"""
        return self._name
    
    @name.setter
    def name(self, value):
        """Set the name with validation"""
        if not value:
            raise ValueError("Name cannot be empty")
        self._name = value
    
    @property
    def age(self):
        """Get the age"""
        return self._age
    
    @age.setter
    def age(self, value):
        """Set the age with validation"""
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value

person = Person("Alice", 30)
print(f"   Name: {person.name}")  # Looks like an attribute!
print(f"   Age: {person.age}")
person.name = "Bob"
person.age = 25

print("   āœ… Clean syntax — looks like normal attributes")
print("   āœ… Validation is automatic — without extra method calls")


# ============================================================
# THE BENEFITS
# ============================================================

print("\n" + "-" * 30)
print("BENEFITS OF @property")
print("-" * 30)
print("""
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ BENEFIT             │ WHAT IT MEANS FOR YOU                    │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Clean syntax        │ obj.property instead of obj.get_property()│
│                     │                                            │
│ Validation          │ Add validation when setting values        │
│                     │                                            │
│ Computed values     │ Calculate values on the fly               │
│                     │                                            │
│ Read-only           │ Create properties that can't be changed   │
│                     │                                            │
│ Backward compatible │ Change attributes to properties without  │
│                     │ breaking existing code                    │
│                     │                                            │
│ Encapsulation       │ Hide implementation details               │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

šŸ“Œ @property gives you the best of both worlds: attribute syntax + method logic.
""")

Benefits of @property:

  • Clean syntax — attribute-like access
  • Validation — add logic when setting values
  • Computed values — calculate on the fly
  • Read-only — prevent changes
  • Backward compatible — change from attribute to property
  • Encapsulation — hide implementation details

Quick Check: What's the main advantage of @property over traditional getters and setters? (Answer: Cleaner syntax — you use dot notation instead of method calls)

The Getter - Reading Values

2

How to Read Values with @property

The getter is the simplest part. You just put @property above a method that returns a value.

# The Getter - Reading Values

print("=" * 50)
print("THE GETTER")
print("=" * 50)

# ============================================================
# BASIC GETTER
# ============================================================

print("\n1. BASIC GETTER")

class Student:
    def __init__(self, name, grade):
        self._name = name
        self._grade = grade
    
    @property
    def name(self):
        """Getter for name"""
        return self._name
    
    @property
    def grade(self):
        """Getter for grade"""
        return self._grade

student = Student("Alice", 95)
print(f"   Name: {student.name}")  # Looks like an attribute
print(f"   Grade: {student.grade}")


# ============================================================
# GETTER WITH FORMATTING
# ============================================================

print("\n2. GETTER WITH FORMATTING")

class Product:
    def __init__(self, name, price):
        self._name = name
        self._price = price
    
    @property
    def name(self):
        return self._name.title()  # Capitalize the name
    
    @property
    def price(self):
        return f"${self._price:.2f}"  # Format as currency

product = Product("python book", 29.99)
print(f"   Product: {product.name}")
print(f"   Price: {product.price}")


# ============================================================
# READ-ONLY PROPERTIES
# ============================================================

print("\n3. READ-ONLY PROPERTIES")

class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):
        return self._radius
    
    @property
    def area(self):
        """Read-only property - calculated on the fly"""
        import math
        return math.pi * self._radius ** 2
    
    @property
    def circumference(self):
        """Read-only property - calculated on the fly"""
        import math
        return 2 * math.pi * self._radius

circle = Circle(5)
print(f"   Radius: {circle.radius}")
print(f"   Area: {circle.area:.2f}")
print(f"   Circumference: {circle.circumference:.2f}")

# Can't set read-only properties
try:
    circle.area = 100
except AttributeError as e:
    print(f"   āŒ Can't set area: {e}")


# ============================================================
# PROPERTIES WITH PROTECTED DATA
# ============================================================

print("\n4. PROPERTIES WITH PROTECTED DATA")

class BankAccount:
    def __init__(self, owner, balance):
        self._owner = owner
        self._balance = balance
        self._transactions = []
    
    @property
    def owner(self):
        return self._owner
    
    @property
    def balance(self):
        return self._balance
    
    @property
    def transaction_count(self):
        return len(self._transactions)
    
    def deposit(self, amount):
        self._balance += amount
        self._transactions.append(f"Deposit: +${amount}")
    
    def withdraw(self, amount):
        if amount <= self._balance:
            self._balance -= amount
            self._transactions.append(f"Withdraw: -${amount}")
        else:
            raise ValueError("Insufficient funds")

account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)

print(f"   Owner: {account.owner}")
print(f"   Balance: ${account.balance}")
print(f"   Transactions: {account.transaction_count}")

Getter key points:

  • @property — turns a method into a read-only attribute
  • Return value — the method returns the value
  • Formatting — can format or transform the value
  • Read-only — properties without setters are read-only
  • Computed — can calculate values on the fly

Quick Check: What decorator do you use for a getter? (Answer: @property)

The Setter - Setting Values

3

How to Set Values with Validation

The setter lets you control how values are set. You can add validation, logging, or any other logic.

# The Setter - Setting Values

print("=" * 50)
print("THE SETTER")
print("=" * 50)

# ============================================================
# BASIC SETTER
# ============================================================

print("\n1. BASIC SETTER")

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        if not value or not value.strip():
            raise ValueError("Name cannot be empty")
        self._name = value.strip()
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise TypeError("Age must be an integer")
        if value < 0 or value > 150:
            raise ValueError("Age must be between 0 and 150")
        self._age = value

person = Person("Alice", 30)
print(f"   Initial: {person.name}, {person.age}")

person.name = "Bob"
person.age = 35
print(f"   Updated: {person.name}, {person.age}")

# Try invalid values
try:
    person.name = ""
except ValueError as e:
    print(f"   āŒ {e}")

try:
    person.age = 200
except ValueError as e:
    print(f"   āŒ {e}")


# ============================================================
# SETTER WITH LOGGING
# ============================================================

print("\n2. SETTER WITH LOGGING")

class Config:
    def __init__(self):
        self._settings = {}
        self._log = []
    
    @property
    def settings(self):
        return self._settings
    
    @settings.setter
    def settings(self, value):
        if not isinstance(value, dict):
            raise TypeError("Settings must be a dictionary")
        self._log.append(f"Settings updated: {value}")
        self._settings = value
    
    @property
    def log(self):
        return self._log

config = Config()
config.settings = {"theme": "dark", "language": "en"}
config.settings = {"theme": "light"}

print(f"   Settings: {config.settings}")
print(f"   Log: {config.log}")


# ============================================================
# SETTER WITH TRANSFORMATION
# ============================================================

print("\n3. SETTER WITH TRANSFORMATION")

class User:
    def __init__(self, email):
        self._email = email
    
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, value):
        # Convert to lowercase and strip whitespace
        clean_email = value.strip().lower()
        if '@' not in clean_email:
            raise ValueError("Invalid email format")
        self._email = clean_email

user = User("Alice@Example.com")
print(f"   Original email: {user.email}")

user.email = "BOB@TEST.COM"
print(f"   After update: {user.email}")  # Converted to lowercase


# ============================================================
# COMPLEX SETTER WITH MULTIPLE RULES
# ============================================================

print("\n4. COMPLEX SETTER WITH MULTIPLE RULES")

class Product:
    def __init__(self, name, price, quantity):
        self._name = name
        self._price = price
        self._quantity = quantity
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        if not value or len(value) < 2:
            raise ValueError("Name must be at least 2 characters")
        self._name = value
    
    @property
    def price(self):
        return self._price
    
    @price.setter
    def price(self, value):
        if value <= 0:
            raise ValueError("Price must be positive")
        if value > 10000:
            raise ValueError("Price cannot exceed $10,000")
        self._price = value
    
    @property
    def quantity(self):
        return self._quantity
    
    @quantity.setter
    def quantity(self, value):
        if value < 0:
            raise ValueError("Quantity cannot be negative")
        self._quantity = value
    
    @property
    def total_value(self):
        return self._price * self._quantity

product = Product("Laptop", 999.99, 10)
print(f"   {product.name}: ${product.price} x {product.quantity} = ${product.total_value}")

product.quantity = 5
print(f"   Updated: ${product.total_value}")

try:
    product.price = -100
except ValueError as e:
    print(f"   āŒ {e}")

Setter key points:

  • @name.setter — defines a setter for the property
  • Validation — check values before setting
  • Transformation — modify values (e.g., lowercase)
  • Logging — track changes
  • Multiple rules — can enforce complex constraints

Quick Check: How do you define a setter for a property called 'name'? (Answer: @name.setter above the setter method)

The Deleter - Deleting Values

4

How to Delete Values with @property

The deleter lets you control what happens when someone tries to delete a property.

# The Deleter - Deleting Values

print("=" * 50)
print("THE DELETER")
print("=" * 50)

# ============================================================
# BASIC DELETER
# ============================================================

print("\n1. BASIC DELETER")

class Data:
    def __init__(self):
        self._value = "Secret"
    
    @property
    def value(self):
        return self._value
    
    @value.setter
    def value(self, new_value):
        self._value = new_value
    
    @value.deleter
    def value(self):
        """Delete the value"""
        print("   Deleting value...")
        self._value = None

data = Data()
print(f"   Value: {data.value}")

data.value = "New secret"
print(f"   Updated: {data.value}")

del data.value
print(f"   After delete: {data.value}")


# ============================================================
# DELETER WITH CLEANUP
# ============================================================

print("\n2. DELETER WITH CLEANUP")

class FileResource:
    def __init__(self, filename):
        self.filename = filename
        self._content = None
        self._is_open = False
    
    @property
    def content(self):
        if not self._is_open:
            self._open_file()
        return self._content
    
    def _open_file(self):
        print(f"   Opening file: {self.filename}")
        self._is_open = True
        self._content = f"Content of {self.filename}"
    
    def _close_file(self):
        print(f"   Closing file: {self.filename}")
        self._is_open = False
    
    @content.deleter
    def content(self):
        """Close the file and delete content"""
        print("   Deleting content...")
        self._close_file()
        self._content = None
        print(f"   File {self.filename} closed")

file = FileResource("data.txt")
print(f"   Content: {file.content}")

del file.content
print("   File resource cleaned up")


# ============================================================
# DELETER WITH PERMISSION CHECK
# ============================================================

print("\n3. DELETER WITH PERMISSION CHECK")

class SecureData:
    def __init__(self, data):
        self._data = data
        self._deleted = False
    
    @property
    def data(self):
        if self._deleted:
            raise AttributeError("Data has been deleted")
        return self._data
    
    @data.deleter
    def data(self):
        """Delete data with confirmation"""
        print("   āš ļø Deleting sensitive data...")
        # In real code, you'd check permissions here
        self._deleted = True
        self._data = None
        print("   āœ… Data deleted")

secure = SecureData("Sensitive information")
print(f"   Data: {secure.data}")

del secure.data

try:
    print(f"   Data: {secure.data}")
except AttributeError as e:
    print(f"   āŒ {e}")


# ============================================================
# COMPLETE EXAMPLE
# ============================================================

print("\n4. COMPLETE EXAMPLE")

class Employee:
    def __init__(self, name, salary):
        self._name = name
        self._salary = salary
        self._terminated = False
    
    @property
    def name(self):
        if self._terminated:
            raise AttributeError("Employee has been terminated")
        return self._name
    
    @name.setter
    def name(self, value):
        if self._terminated:
            raise AttributeError("Cannot modify terminated employee")
        if not value:
            raise ValueError("Name cannot be empty")
        self._name = value
    
    @property
    def salary(self):
        if self._terminated:
            raise AttributeError("Employee has been terminated")
        return self._salary
    
    @salary.setter
    def salary(self, value):
        if self._terminated:
            raise AttributeError("Cannot modify terminated employee")
        if value < 0:
            raise ValueError("Salary cannot be negative")
        self._salary = value
    
    @salary.deleter
    def salary(self):
        """Delete salary (terminate employee)"""
        print(f"   āŒ Terminating {self._name}")
        self._terminated = True
        self._salary = 0
        del self._name

emp = Employee("Alice", 50000)
print(f"   Employee: {emp.name}, Salary: ${emp.salary}")

try:
    emp.salary = -1000
except ValueError as e:
    print(f"   āŒ {e}")

del emp.salary  # Terminate employee

try:
    print(f"   Name: {emp.name}")
except AttributeError as e:
    print(f"   āŒ {e}")

Deleter key points:

  • @name.deleter — defines a deleter for the property
  • Cleanup — release resources or reset state
  • Permission checks — verify before deleting
  • State management — update internal state
  • Exception handling — raise errors for invalid operations

Quick Check: What decorator is used for a deleter? (Answer: @name.deleter)

Computed Properties

5

Values Calculated on the Fly

Computed properties are values that are calculated when you access them. They're not stored, they're calculated each time.

# Computed Properties

print("=" * 50)
print("COMPUTED PROPERTIES")
print("=" * 50)

# ============================================================
# BASIC COMPUTED PROPERTIES
# ============================================================

print("\n1. BASIC COMPUTED PROPERTIES")

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    @property
    def width(self):
        return self._width
    
    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("Width must be positive")
        self._width = value
    
    @property
    def height(self):
        return self._height
    
    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("Height must be positive")
        self._height = value
    
    @property
    def area(self):
        """Computed property"""
        return self._width * self._height
    
    @property
    def perimeter(self):
        """Computed property"""
        return 2 * (self._width + self._height)

rect = Rectangle(5, 3)
print(f"   Width: {rect.width}, Height: {rect.height}")
print(f"   Area: {rect.area}")
print(f"   Perimeter: {rect.perimeter}")

rect.width = 10
print(f"   After update - Area: {rect.area}")


# ============================================================
# COMPUTED PROPERTIES WITH CACHING
# ============================================================

print("\n2. COMPUTED PROPERTIES WITH CACHING")

class ExpensiveCalculation:
    def __init__(self, value):
        self._value = value
        self._cached_result = None
        self._cache_valid = False
    
    @property
    def value(self):
        return self._value
    
    @value.setter
    def value(self, new_value):
        self._value = new_value
        self._cache_valid = False  # Invalidate cache
    
    @property
    def result(self):
        """Expensive calculation with caching"""
        if not self._cache_valid:
            print("   šŸ’» Calculating... (expensive operation)")
            import time
            time.sleep(0.5)  # Simulate expensive calculation
            self._cached_result = self._value ** 2
            self._cache_valid = True
        else:
            print("   šŸ’¾ Using cached result")
        return self._cached_result

calc = ExpensiveCalculation(5)
print(f"   First access: {calc.result}")  # Calculates
print(f"   Second access: {calc.result}")  # Uses cache
calc.value = 10
print(f"   After value change: {calc.result}")  # Recalculates


# ============================================================
# COMPUTED PROPERTIES WITH DEPENDENCIES
# ============================================================

print("\n3. COMPUTED PROPERTIES WITH DEPENDENCIES")

class Order:
    def __init__(self):
        self._items = []
        self._tax_rate = 0.08
    
    def add_item(self, name, price, quantity=1):
        self._items.append({"name": name, "price": price, "quantity": quantity})
    
    @property
    def subtotal(self):
        return sum(item["price"] * item["quantity"] for item in self._items)
    
    @property
    def tax(self):
        return self.subtotal * self._tax_rate
    
    @property
    def total(self):
        return self.subtotal + self.tax
    
    @property
    def item_count(self):
        return len(self._items)
    
    @property
    def total_items(self):
        return sum(item["quantity"] for item in self._items)

order = Order()
order.add_item("Laptop", 999.99)
order.add_item("Mouse", 29.99, 2)

print(f"   Items: {order.item_count} items")
print(f"   Total items: {order.total_items}")
print(f"   Subtotal: ${order.subtotal:.2f}")
print(f"   Tax: ${order.tax:.2f}")
print(f"   Total: ${order.total:.2f}")


# ============================================================
# READ-ONLY COMPUTED PROPERTIES
# ============================================================

print("\n4. READ-ONLY COMPUTED PROPERTIES")

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    
    @property
    def celsius(self):
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        self._celsius = value
    
    @property
    def fahrenheit(self):
        """Read-only computed property"""
        return self._celsius * 9/5 + 32
    
    @property
    def kelvin(self):
        """Read-only computed property"""
        return self._celsius + 273.15

temp = Temperature(25)
print(f"   Celsius: {temp.celsius}°C")
print(f"   Fahrenheit: {temp.fahrenheit}°F")
print(f"   Kelvin: {temp.kelvin}K")

temp.celsius = 0
print(f"   After update:")
print(f"   Fahrenheit: {temp.fahrenheit}°F")

Computed properties key points:

  • On-the-fly — calculated when accessed
  • Caching — store results for performance
  • Dependencies — update when dependencies change
  • Read-only — computed values often don't have setters
  • Convenience — provide derived values easily

Quick Check: What is a computed property? (Answer: A property whose value is calculated when accessed, not stored)

Real-World Example

6

Building a User Profile System

# Real-World Example: User Profile System

from datetime import datetime
import re

print("=" * 60)
print("USER PROFILE SYSTEM")
print("=" * 60)

# ============================================================
# USER PROFILE WITH @property
# ============================================================

class UserProfile:
    """Complete user profile with property-based encapsulation"""
    
    def __init__(self, username, email, birth_date):
        self._username = None
        self._email = None
        self._birth_date = None
        self._last_login = None
        self._login_count = 0
        self._is_active = True
        
        # Use setters for validation
        self.username = username
        self.email = email
        self.birth_date = birth_date
    
    # ----- USERNAME -----
    @property
    def username(self):
        return self._username
    
    @username.setter
    def username(self, value):
        if not value or len(value) < 3:
            raise ValueError("Username must be at least 3 characters")
        if not re.match(r'^[a-zA-Z0-9_]+$', value):
            raise ValueError("Username can only contain letters, numbers, and underscores")
        self._username = value.lower()
    
    # ----- EMAIL -----
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, value):
        if '@' not in value or '.' not in value:
            raise ValueError("Invalid email format")
        self._email = value.strip().lower()
    
    # ----- BIRTH DATE -----
    @property
    def birth_date(self):
        return self._birth_date
    
    @birth_date.setter
    def birth_date(self, value):
        try:
            # Try to parse the date
            if isinstance(value, str):
                date_obj = datetime.strptime(value, "%Y-%m-%d")
            else:
                date_obj = value
        except:
            raise ValueError("Invalid date format. Use YYYY-MM-DD")
        
        # Check age
        today = datetime.now()
        age = today.year - date_obj.year - ((today.month, today.day) < (date_obj.month, date_obj.day))
        if age < 13:
            raise ValueError("User must be at least 13 years old")
        if age > 120:
            raise ValueError("Invalid birth date")
        
        self._birth_date = date_obj
    
    # ----- COMPUTED PROPERTIES -----
    @property
    def age(self):
        """Calculate age from birth date"""
        today = datetime.now()
        return today.year - self._birth_date.year - ((today.month, today.day) < (self._birth_date.month, self._birth_date.day))
    
    @property
    def age_group(self):
        """Categorize user by age"""
        age = self.age
        if age < 18:
            return "Minor"
        elif age < 25:
            return "Young Adult"
        elif age < 40:
            return "Adult"
        elif age < 65:
            return "Middle Age"
        else:
            return "Senior"
    
    @property
    def profile_complete(self):
        """Check if profile is complete"""
        return all([self._username, self._email, self._birth_date])
    
    @property
    def login_count(self):
        return self._login_count
    
    @property
    def last_login(self):
        return self._last_login
    
    @property
    def is_active(self):
        return self._is_active
    
    # ----- PUBLIC METHODS -----
    def login(self):
        """Record a login"""
        self._last_login = datetime.now()
        self._login_count += 1
        if not self._is_active:
            self._is_active = True
        return f"Welcome back, {self.username}!"
    
    def logout(self):
        """Logout user"""
        return f"Goodbye, {self.username}!"
    
    def deactivate(self):
        """Deactivate the account"""
        self._is_active = False
        return f"Account for {self.username} deactivated"
    
    def get_profile_summary(self):
        """Get a summary of the profile"""
        return {
            "username": self.username,
            "email": self.email,
            "age": self.age,
            "age_group": self.age_group,
            "profile_complete": self.profile_complete,
            "login_count": self.login_count,
            "is_active": self.is_active,
            "last_login": self.last_login.strftime("%Y-%m-%d %H:%M") if self.last_login else "Never"
        }


# ============================================================
# DEMONSTRATION
# ============================================================

print("\n1. CREATING A USER PROFILE")
try:
    user = UserProfile("alice_123", "alice@example.com", "2000-05-15")
    print(f"   āœ… User created: {user.username}")
    print(f"   šŸ“§ Email: {user.email}")
    print(f"   šŸŽ‚ Age: {user.age} ({user.age_group})")
except ValueError as e:
    print(f"   āŒ Error: {e}")

print("\n2. VALIDATION TESTS")
try:
    user2 = UserProfile("al", "bob@test.com", "1990-01-01")
except ValueError as e:
    print(f"   āŒ Username too short: {e}")

try:
    user3 = UserProfile("bob_123", "invalid-email", "1990-01-01")
except ValueError as e:
    print(f"   āŒ Invalid email: {e}")

try:
    user4 = UserProfile("charlie_123", "charlie@test.com", "2015-01-01")
except ValueError as e:
    print(f"   āŒ Too young: {e}")

print("\n3. USER ACTIVITY")
print(f"   {user.login()}")
print(f"   {user.login()}")
print(f"   Logins: {user.login_count}")

print("\n4. PROFILE SUMMARY")
summary = user.get_profile_summary()
for key, value in summary.items():
    print(f"   {key}: {value}")

print("\n5. UPDATING PROFILE")
try:
    user.email = "new_email@example.com"
    print(f"   āœ… Email updated: {user.email}")
except ValueError as e:
    print(f"   āŒ {e}")

print("\n6. DEACTIVATING ACCOUNT")
print(f"   {user.deactivate()}")
print(f"   Active: {user.is_active}")

print("\n" + "=" * 60)
print("KEY TAKEAWAYS:")
print("=" * 60)
print("āœ… @property provides clean attribute-like access")
print("āœ… Validation ensures data quality")
print("āœ… Computed properties provide derived values")
print("āœ… Getters, setters, and deleters give full control")
print("āœ… Encapsulation hides implementation details")
print("āœ… The code is clean, readable, and maintainable")

Real-world example key points:

  • Validation — username, email, birth date all validated
  • Computed properties — age, age_group, profile_complete
  • Read-only — login_count, last_login, is_active
  • Encapsulation — internal state is protected
  • Clean interface — users interact with simple attributes

Quick Check: What does the age property do in the UserProfile class? (Answer: It calculates the user's age from their birth date)

Best Practices

7

Using @property Effectively

# Best Practices for @property

print("=" * 60)
print("BEST PRACTICES FOR @property")
print("=" * 60)

# ============================================================
# 1. KEEP PROPERTIES SIMPLE
# ============================================================

print("\n1. KEEP PROPERTIES SIMPLE")

# āœ… GOOD: Simple getters and setters
class Good:
    def __init__(self, value):
        self._value = value
    
    @property
    def value(self):
        return self._value
    
    @value.setter
    def value(self, new_value):
        if new_value < 0:
            raise ValueError("Value must be positive")
        self._value = new_value

# āŒ BAD: Complex operations in properties
class Bad:
    @property
    def value(self):
        # Doing heavy computation here is bad
        # Properties should be light and fast
        return self._heavy_computation()

print("   āœ… Properties should be simple and fast")


# ============================================================
# 2. DON'T USE PROPERTIES FOR EXPENSIVE OPERATIONS
# ============================================================

print("\n2. DON'T USE PROPERTIES FOR EXPENSIVE OPERATIONS")

# āŒ BAD: Expensive calculation in property
class BadReport:
    def __init__(self, data):
        self._data = data
    
    @property
    def processed_data(self):
        # This might be slow - better as a method
        return self._process_all_data()

# āœ… GOOD: Use methods for expensive operations
class GoodReport:
    def __init__(self, data):
        self._data = data
    
    def process_data(self):
        """This is clearly an operation, not a property"""
        return self._process_all_data()

print("   āœ… Use methods for expensive operations, not properties")


# ============================================================
# 3. BE CONSISTENT WITH NAMING
# ============================================================

print("\n3. BE CONSISTENT WITH NAMING")

# āœ… GOOD: Clear, consistent naming
class Product:
    def __init__(self, name, price):
        self._name = name
        self._price = price
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        self._name = value
    
    @property
    def price(self):
        return self._price
    
    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price = value

print("   āœ… Use the same name for getter and setter")


# ============================================================
# 4. USE READ-ONLY PROPERTIES FOR DERIVED VALUES
# ============================================================

print("\n4. USE READ-ONLY PROPERTIES FOR DERIVED VALUES")

class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value
    
    @property
    def area(self):
        """Read-only computed property"""
        import math
        return math.pi * self._radius ** 2

print("   āœ… Use read-only properties for derived values")


# ============================================================
# 5. DON'T OVERUSE @property
# ============================================================

print("\n5. DON'T OVERUSE @property")

# āŒ BAD: Using @property for everything (overkill)
class Overkill:
    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c
    
    @property
    def a(self):
        return self._a
    
    @a.setter
    def a(self, value):
        self._a = value
    
    @property
    def b(self):
        return self._b
    
    @b.setter
    def b(self, value):
        self._b = value
    
    @property
    def c(self):
        return self._c
    
    @c.setter
    def c(self, value):
        self._c = value

# āœ… GOOD: Only use @property when you need control
class Simple:
    def __init__(self, a, b, c):
        self.a = a  # Public attribute - no need for property
        self.b = b
        self.c = c

print("   āœ… Only use @property when you need control")


# ============================================================
# 6. SUMMARY
# ============================================================

print("\n" + "=" * 60)
print("BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ PRACTICE                    │ WHY IT MATTERS                             │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Keep properties simple      │ Should be fast and lightweight            │
│                             │                                             │
│ Don't use for expensive     │ Use methods for heavy operations          │
│ operations                  │                                             │
│                             │                                             │
│ Be consistent with naming   │ Makes code predictable and readable       │
│                             │                                             │
│ Use read-only for derived   │ Clear intent and prevents modification    │
│ values                      │                                             │
│                             │                                             │
│ Don't overuse @property     │ Use when you need control, not by default │
│                             │                                             │
│ Document your properties    │ Help others understand your code          │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

šŸ“Œ @property is powerful, but use it when you need control over attribute access.
""")

Best practices summary:

  • Keep properties simple — they should be fast
  • Don't use for expensive operations — use methods instead
  • Be consistent with naming — make it predictable
  • Use read-only for derived values — clear intent
  • Don't overuse — use when you need control
  • Document properties — help others understand

Quick Check: When should you use @property? (Answer: When you need control over attribute access, like validation or computed values)

Try It Yourself

Experiment with the @property decorator in the editor below.

Loading Pyodide... 0%
Python Code Editor
==================================================
PROPERTY DECORATOR - PRACTICE
==================================================

1. BASIC GETTER AND SETTER
Owner: Alice
Balance: $1000
New balance: $1300
āŒ Balance cannot be negative

2. COMPUTED PROPERTY
Side: 5
Area: 25
Perimeter: 20
After update - Area: 100

3. DELETER
User: Alice
Closing session for Alice
āŒ Session is closed
šŸ†

You've Got It!

You now understand the @property decorator in Python. You know how to use getters, setters, and deleters, and how to create computed properties.

Quick Quiz

Test what you've learned:

1. What decorator is used to create a property getter?
2. How do you define a setter for a property called 'age'?
3. What is a computed property?
4. What does @name.deleter do?
5. When should you use @property?

Frequently Asked Questions

What is the @property decorator in Python? ā–¼

The @property decorator allows you to define methods that can be accessed like attributes. It's used for getters, setters, and deleters, providing a clean way to add logic to attribute access.

What's the difference between @property and regular methods? ā–¼

@property methods are accessed like attributes (obj.property) instead of methods (obj.get_property()). This makes the code cleaner and more intuitive. They're great for when you want to add logic without changing the syntax.

Can a property have both a getter and a setter? ā–¼

Yes! You can define both a getter and a setter for a property. The getter uses @property and the setter uses @name.setter. This gives you full control over reading and writing the property.

What is a computed property? ā–¼

A computed property is a property whose value is calculated when accessed, rather than stored. For example, an area property that calculates the area from width and height.

Should I use @property for all attributes? ā–¼

No! Only use @property when you need control over access. For simple attributes that don't need validation or logic, just use regular public attributes. Overusing @property makes your code unnecessarily complex.

Can I use @property with dataclasses? ā–¼

Yes! You can use @property with dataclasses. Just define the property as a method inside the dataclass. The property will work alongside the dataclass's automatically generated methods.

Where to Go From Here

Now that you understand the @property decorator in Python, check out these related topics:

Decorators

Learn about decorators — the foundation of @property.

Learn More →

Encapsulation

Learn how @property helps with encapsulation.

Learn More →

Dataclasses

Learn how dataclasses work with @property.

Learn More →
Interview Resources
  • Python Syntax & Variables Interview Questions
  • Top SQL Interview Questions & Answers
  • SQL Joins: Displaying Data from Multiple Tables FAQ
  • Python Lists and Dictionaries Interview Questions
  • Python OOP Interview Questions
  • SQL Set Operators Interview Questions
Previous: Context Managers Next: __slots__ →