Your Circle class has radius. You want area to look like an attribute but compute on access. You want radius to validate on assignment. @property turns methods into attribute-like access - clean API with hidden logic.

Basic property

Turn a method into a read-only attribute.

person
basics.py
Replay: real traced execution (multi-file project)
# Basic @property Usage

class Person:
    """Demonstrates basic property usage."""

    def __init__(self, name, age):
        self._name = name
        self._age = age

    @property
    def name(self):
        """Get the person's name."""
        print(f"  Getting name: {self._name}")
        return self._name

    @name.setter
    def name(self, value):
        """Set the person's name."""
        print(f"  Setting name: {value}")
        self._name = value

    @property
    def age(self):
        """Get the person's age."""
        return self._age

    @age.setter
    def age(self, value):
        """Set the person's age."""
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value


def main():
    print("=== Basic @property Usage ===\n")

    # Create person
    print("--- Creating Person ---")
    person = Person("Alice", 30)
    print(f"Created: {person._name}, {person._age}\n")

    # Access via property (calls getter)
    print("--- Getting via Property ---")
    name = person.name
    print(f"Name is: {name}\n")

    # Set via property (calls setter)
    print("--- Setting via Property ---")
    person.name = "Bob"
    print(f"Name changed to: {person.name}\n")

    # Age with validation
    print("--- Age with Validation ---")
    print(f"Current age: {person.age}")
    person.age = 31
    print(f"New age: {person.age}")

    try:
        person.age = -5
    except ValueError as e:
        print(f"Error: {e}")

    print("\n=== Key Points ===")
    print("""
1. @property decorator converts method to getter
2. @<property>.setter creates setter
3. Access like regular attribute (no parentheses)
4. Underlying value often stored with _ prefix
5. Enables validation, logging, etc.
    """)


if __name__ == "__main__":
    main()







































# Basic @property Usage

class Person:
    """Demonstrates basic property usage."""

    def __init__(self, name, age):
        self._name = name
        self._age = age

    @property
    def name(self):
        """Get the person's name."""
        print(f"  Getting name: {self._name}")
        return self._name

    @name.setter
    def name(self, value):
        """Set the person's name."""
        print(f"  Setting name: {value}")
        self._name = value

    @property
    def age(self):
        """Get the person's age."""
        return self._age

    @age.setter
    def age(self, value):
        """Set the person's age."""
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value


def main():
    print("=== Basic @property Usage ===\n")

    # Create person
    print("--- Creating Person ---")
    person = Person("Dana", 42)
    print(f"Created: {person._name}, {person._age}\n")

    # Access via property (calls getter)
    print("--- Getting via Property ---")
    name = person.name
    print(f"Name is: {name}\n")

    # Set via property (calls setter)
    print("--- Setting via Property ---")
    person.name = "Bob"
    print(f"Name changed to: {person.name}\n")

    # Age with validation
    print("--- Age with Validation ---")
    print(f"Current age: {person.age}")
    person.age = 31
    print(f"New age: {person.age}")

    try:
        person.age = -5
    except ValueError as e:
        print(f"Error: {e}")

    print("\n=== Key Points ===")
    print("""
1. @property decorator converts method to getter
2. @<property>.setter creates setter
3. Access like regular attribute (no parentheses)
4. Underlying value often stored with _ prefix
5. Enables validation, logging, etc.
    """)


if __name__ == "__main__":
    main()







































# Basic @property Usage

class Person:
    """Demonstrates basic property usage."""

    def __init__(self, name, age):
        self._name = name
        self._age = age

    @property
    def name(self):
        """Get the person's name."""
        print(f"  Getting name: {self._name}")
        return self._name

    @name.setter
    def name(self, value):
        """Set the person's name."""
        print(f"  Setting name: {value}")
        self._name = value

    @property
    def age(self):
        """Get the person's age."""
        return self._age

    @age.setter
    def age(self, value):
        """Set the person's age."""
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value


def main():
    print("=== Basic @property Usage ===\n")

    # Create person
    print("--- Creating Person ---")
    person = Person("Sam", 19)
    print(f"Created: {person._name}, {person._age}\n")

    # Access via property (calls getter)
    print("--- Getting via Property ---")
    name = person.name
    print(f"Name is: {name}\n")

    # Set via property (calls setter)
    print("--- Setting via Property ---")
    person.name = "Bob"
    print(f"Name changed to: {person.name}\n")

    # Age with validation
    print("--- Age with Validation ---")
    print(f"Current age: {person.age}")
    person.age = 31
    print(f"New age: {person.age}")

    try:
        person.age = -5
    except ValueError as e:
        print(f"Error: {e}")

    print("\n=== Key Points ===")
    print("""
1. @property decorator converts method to getter
2. @<property>.setter creates setter
3. Access like regular attribute (no parentheses)
4. Underlying value often stored with _ prefix
5. Enables validation, logging, etc.
    """)


if __name__ == "__main__":
    main()







































  1. """Demonstrates basic property usage."""

    3class Person:4    """Demonstrates basic property usage."""
  2. def main():

    35def main():36    print("=== Basic @property Usage ===\n")3738    # Create person #?create_person39    print("--- Creating Person ---")40    person = Person("Alice", 30) #?create_alice41    #@person=Person("Dana", 42), Person("Sam", 19)
    output=== Basic @property Usage ===
    --- Creating Person ---
  3. self._name ← Alice, self._age ← 30

    6def __init__(self⟨Person A⟩, nameAlice, age30): #?init7    self._name→ Alice = nameAlice #?set_name8    self._age→ 30 = age30 #?set_age
  4. person ← ⟨Person A⟩

    39print("--- Creating Person ---")40person→ ⟨Person A⟩ = Person("Alice", 30) #?create_alice41#@person=Person("Dana", 42), Person("Sam", 19)42print(f"Created: {person._nameAlice}, {person._age30}\n") #?print_created4344# Access via property (calls getter) #?access_property45print("--- Getting via Property ---")46name = person.nameAlice #?get_name47print(f"Name is: {name}\n") #?print_name
    outputCreated: Alice, 30
    --- Getting via Property ---
  5. def name(self): #?name_getter

    pass 1 of 2
    10@property #?property_decorator11def name(self⟨Person A⟩): #?name_getter12    """Get the person's name.""" #?getter_doc13    print(f"  Getting name: {self._nameAlice}") #?log_get_name14    return self._nameAlice #?return_name
    output  Getting name: Alice
  6. name ← Alice

    45print("--- Getting via Property ---")46name→ Alice = person.nameAlice #?get_name47print(f"Name is: {nameAlice}\n") #?print_name4849# Set via property (calls setter) #?set_property50print("--- Setting via Property ---")51person.name = "Bob" #?set_bob52print(f"Name changed to: {person.name}\n") #?print_changed
    outputName is: Alice
    --- Setting via Property ---
  7. self._name ← Bob

    16@name.setter #?name_setter_decorator17def name(self⟨Person A⟩, valueBob): #?name_setter18    """Set the person's name.""" #?setter_doc19    print(f"  Setting name: {valueBob}") #?log_set_name20    self._name→ Bob = valueBob #?store_name
    output  Setting name: Bob
  8. person.name ← Bob

    50print("--- Setting via Property ---")51person.name→ Bob = "Bob" #?set_bob52print(f"Name changed to: {person.nameBob}\n") #?print_changed
  9. def name(self): #?name_getter

    pass 2 of 2
    10@property #?property_decorator11def name(self⟨Person A⟩): #?name_getter12    """Get the person's name.""" #?getter_doc13    print(f"  Getting name: {self._nameBob}") #?log_get_name14    return self._nameBob #?return_name
    output  Getting name: Bob
  10. print(f"Name changed to: {person.name} ") #?print_changed

    51person.name = "Bob" #?set_bob52print(f"Name changed to: {person.nameBob}\n") #?print_changed5354# Age with validation #?age_validation55print("--- Age with Validation ---")56print(f"Current age: {person.age30}") #?print_current_age57person.age = 31 #?set_valid_age
    outputName changed to: Bob
    --- Age with Validation ---
  11. def age(self): #?age_getter

    pass 1 of 2
    22@property #?age_property23def age(self⟨Person A⟩): #?age_getter24    """Get the person's age."""25    return self._age30 #?return_age
  12. print(f"Current age: {person.age}") #?print_current_age

    55print("--- Age with Validation ---")56print(f"Current age: {person.age30}") #?print_current_age57person.age = 31 #?set_valid_age58print(f"New age: {person.age}") #?print_new_age
    outputCurrent age: 30
  13. self._age ← 31

    pass 1 of 2
    27@age.setter #?age_setter_decorator28def age(self⟨Person A⟩, value31): #?age_setter29    """Set the person's age."""30    if value < 0: #?validate_age31        raise ValueError("Age cannot be negative") #?raise_negative32    self._age→ 31 = value31 #?store_age
  14. person.age ← 31

    56print(f"Current age: {person.age}") #?print_current_age57person.age→ 31 = 31 #?set_valid_age58print(f"New age: {person.age31}") #?print_new_age
  15. def age(self): #?age_getter

    pass 2 of 2
    22@property #?age_property23def age(self⟨Person A⟩): #?age_getter24    """Get the person's age."""25    return self._age31 #?return_age
  16. print(f"New age: {person.age}") #?print_new_age

    57person.age = 31 #?set_valid_age58print(f"New age: {person.age31}") #?print_new_age
    outputNew age: 31
  17. def age(self, value): #?age_setter

    pass 2 of 2
    27@age.setter #?age_setter_decorator28def age(self⟨Person A⟩, value-5): #?age_setter29    """Set the person's age."""30    if value < 0: #?validate_age
  18. if value < 0: #?validate_age

    29"""Set the person's age."""30if value-5 < 0: #?validate_age31    raise ValueError("Age cannot be negative") #?raise_negative32self._age = value #?store_age
  19. except ValueError as e: #?catch_invalid

    61    person.age = -5 #?set_invalid_age62except ValueError as e: #?catch_invalid63    print(f"Error: {eAge cannot be negative}") #?print_error6465print("\n=== Key Points ===")
    outputError: Age cannot be negative
    Error: Age cannot be negative
  20. print(" === Key Points ===")

    65    print("\n=== Key Points ===")66    print("""671. @property decorator converts method to getter682. @<property>.setter creates setter693. Access like regular attribute (no parentheses)704. Underlying value often stored with _ prefix715. Enables validation, logging, etc.72    """)
    output
    === Key Points ===
    
    1. @property decorator converts method to getter
    2. @<property>.setter creates setter
    3. Access like regular attribute (no parentheses)
    4. Underlying value often stored with _ prefix
    5. Enables validation, logging, etc.
        
  21. main()

    75if __name__ == "__main__":76    main()
  1. """Demonstrates basic property usage."""

    3class Person:4    """Demonstrates basic property usage."""
  2. def main():

    35def main():36    print("=== Basic @property Usage ===\n")3738    # Create person39    print("--- Creating Person ---")40    person = Person("Dana", 42)41    print(f"Created: {person._name}, {person._age}\n")
    output=== Basic @property Usage ===
    --- Creating Person ---
  3. self._name ← Dana, self._age ← 42

    6def __init__(self⟨Person A⟩, nameDana, age42):7    self._name→ Dana = nameDana8    self._age→ 42 = age42
  4. person ← ⟨Person A⟩

    39print("--- Creating Person ---")40person→ ⟨Person A⟩ = Person("Dana", 42)41print(f"Created: {person._nameDana}, {person._age42}\n")4243# Access via property (calls getter)44print("--- Getting via Property ---")45name = person.nameDana46print(f"Name is: {name}\n")
    outputCreated: Dana, 42
    --- Getting via Property ---
  5. def name(self):

    pass 1 of 2
    10@property11def name(self⟨Person A⟩):12    """Get the person's name."""13    print(f"  Getting name: {self._nameDana}")14    return self._nameDana
    output  Getting name: Dana
  6. name ← Dana

    44print("--- Getting via Property ---")45name→ Dana = person.nameDana46print(f"Name is: {nameDana}\n")4748# Set via property (calls setter)49print("--- Setting via Property ---")50person.name = "Bob"51print(f"Name changed to: {person.name}\n")
    outputName is: Dana
    --- Setting via Property ---
  7. self._name ← Bob

    16@name.setter17def name(self⟨Person A⟩, valueBob):18    """Set the person's name."""19    print(f"  Setting name: {valueBob}")20    self._name→ Bob = valueBob
    output  Setting name: Bob
  8. person.name ← Bob

    49print("--- Setting via Property ---")50person.name→ Bob = "Bob"51print(f"Name changed to: {person.nameBob}\n")
  9. def name(self):

    pass 2 of 2
    10@property11def name(self⟨Person A⟩):12    """Get the person's name."""13    print(f"  Getting name: {self._nameBob}")14    return self._nameBob
    output  Getting name: Bob
  10. print(f"Name changed to: {person.name} ")

    50person.name = "Bob"51print(f"Name changed to: {person.nameBob}\n")5253# Age with validation54print("--- Age with Validation ---")55print(f"Current age: {person.age42}")56person.age = 31
    outputName changed to: Bob
    --- Age with Validation ---
  11. def age(self):

    pass 1 of 2
    22@property23def age(self⟨Person A⟩):24    """Get the person's age."""25    return self._age42
  12. print(f"Current age: {person.age}")

    54print("--- Age with Validation ---")55print(f"Current age: {person.age42}")56person.age = 3157print(f"New age: {person.age}")
    outputCurrent age: 42
  13. self._age ← 31

    pass 1 of 2
    27@age.setter28def age(self⟨Person A⟩, value31):29    """Set the person's age."""30    if value < 0:31        raise ValueError("Age cannot be negative")32    self._age→ 31 = value31
  14. person.age ← 31

    55print(f"Current age: {person.age}")56person.age→ 31 = 3157print(f"New age: {person.age31}")
  15. def age(self):

    pass 2 of 2
    22@property23def age(self⟨Person A⟩):24    """Get the person's age."""25    return self._age31
  16. print(f"New age: {person.age}")

    56person.age = 3157print(f"New age: {person.age31}")
    outputNew age: 31
  17. def age(self, value):

    pass 2 of 2
    27@age.setter28def age(self⟨Person A⟩, value-5):29    """Set the person's age."""30    if value < 0:
  18. if value < 0:

    29"""Set the person's age."""30if value-5 < 0:31    raise ValueError("Age cannot be negative")32self._age = value
  19. except ValueError as e:

    60    person.age = -561except ValueError as e:62    print(f"Error: {eAge cannot be negative}")6364print("\n=== Key Points ===")
    outputError: Age cannot be negative
    Error: Age cannot be negative
  20. print(" === Key Points ===")

    64    print("\n=== Key Points ===")65    print("""661. @property decorator converts method to getter672. @<property>.setter creates setter683. Access like regular attribute (no parentheses)694. Underlying value often stored with _ prefix705. Enables validation, logging, etc.71    """)
    output
    === Key Points ===
    
    1. @property decorator converts method to getter
    2. @<property>.setter creates setter
    3. Access like regular attribute (no parentheses)
    4. Underlying value often stored with _ prefix
    5. Enables validation, logging, etc.
        
  21. main()

    74if __name__ == "__main__":75    main()
  1. """Demonstrates basic property usage."""

    3class Person:4    """Demonstrates basic property usage."""
  2. def main():

    35def main():36    print("=== Basic @property Usage ===\n")3738    # Create person39    print("--- Creating Person ---")40    person = Person("Sam", 19)41    print(f"Created: {person._name}, {person._age}\n")
    output=== Basic @property Usage ===
    --- Creating Person ---
  3. self._name ← Sam, self._age ← 19

    6def __init__(self⟨Person A⟩, nameSam, age19):7    self._name→ Sam = nameSam8    self._age→ 19 = age19
  4. person ← ⟨Person A⟩

    39print("--- Creating Person ---")40person→ ⟨Person A⟩ = Person("Sam", 19)41print(f"Created: {person._nameSam}, {person._age19}\n")4243# Access via property (calls getter)44print("--- Getting via Property ---")45name = person.nameSam46print(f"Name is: {name}\n")
    outputCreated: Sam, 19
    --- Getting via Property ---
  5. def name(self):

    pass 1 of 2
    10@property11def name(self⟨Person A⟩):12    """Get the person's name."""13    print(f"  Getting name: {self._nameSam}")14    return self._nameSam
    output  Getting name: Sam
  6. name ← Sam

    44print("--- Getting via Property ---")45name→ Sam = person.nameSam46print(f"Name is: {nameSam}\n")4748# Set via property (calls setter)49print("--- Setting via Property ---")50person.name = "Bob"51print(f"Name changed to: {person.name}\n")
    outputName is: Sam
    --- Setting via Property ---
  7. self._name ← Bob

    16@name.setter17def name(self⟨Person A⟩, valueBob):18    """Set the person's name."""19    print(f"  Setting name: {valueBob}")20    self._name→ Bob = valueBob
    output  Setting name: Bob
  8. person.name ← Bob

    49print("--- Setting via Property ---")50person.name→ Bob = "Bob"51print(f"Name changed to: {person.nameBob}\n")
  9. def name(self):

    pass 2 of 2
    10@property11def name(self⟨Person A⟩):12    """Get the person's name."""13    print(f"  Getting name: {self._nameBob}")14    return self._nameBob
    output  Getting name: Bob
  10. print(f"Name changed to: {person.name} ")

    50person.name = "Bob"51print(f"Name changed to: {person.nameBob}\n")5253# Age with validation54print("--- Age with Validation ---")55print(f"Current age: {person.age19}")56person.age = 31
    outputName changed to: Bob
    --- Age with Validation ---
  11. def age(self):

    pass 1 of 2
    22@property23def age(self⟨Person A⟩):24    """Get the person's age."""25    return self._age19
  12. print(f"Current age: {person.age}")

    54print("--- Age with Validation ---")55print(f"Current age: {person.age19}")56person.age = 3157print(f"New age: {person.age}")
    outputCurrent age: 19
  13. self._age ← 31

    pass 1 of 2
    27@age.setter28def age(self⟨Person A⟩, value31):29    """Set the person's age."""30    if value < 0:31        raise ValueError("Age cannot be negative")32    self._age→ 31 = value31
  14. person.age ← 31

    55print(f"Current age: {person.age}")56person.age→ 31 = 3157print(f"New age: {person.age31}")
  15. def age(self):

    pass 2 of 2
    22@property23def age(self⟨Person A⟩):24    """Get the person's age."""25    return self._age31
  16. print(f"New age: {person.age}")

    56person.age = 3157print(f"New age: {person.age31}")
    outputNew age: 31
  17. def age(self, value):

    pass 2 of 2
    27@age.setter28def age(self⟨Person A⟩, value-5):29    """Set the person's age."""30    if value < 0:
  18. if value < 0:

    29"""Set the person's age."""30if value-5 < 0:31    raise ValueError("Age cannot be negative")32self._age = value
  19. except ValueError as e:

    60    person.age = -561except ValueError as e:62    print(f"Error: {eAge cannot be negative}")6364print("\n=== Key Points ===")
    outputError: Age cannot be negative
    Error: Age cannot be negative
  20. print(" === Key Points ===")

    64    print("\n=== Key Points ===")65    print("""661. @property decorator converts method to getter672. @<property>.setter creates setter683. Access like regular attribute (no parentheses)694. Underlying value often stored with _ prefix705. Enables validation, logging, etc.71    """)
    output
    === Key Points ===
    
    1. @property decorator converts method to getter
    2. @<property>.setter creates setter
    3. Access like regular attribute (no parentheses)
    4. Underlying value often stored with _ prefix
    5. Enables validation, logging, etc.
        
  21. main()

    74if __name__ == "__main__":75    main()

@property decorator makes method accessible like attribute.

property Attribute that runs code on access. Getter, setter, deleter methods.

Property with validation

Validate values before setting.

validation.py
Replay: real traced execution (multi-file project)
# Properties with Validation

class BankAccount:
    """Bank account with validation."""

    def __init__(self, account_number, balance=0):
        self._account_number = account_number
        self._balance = balance

    @property
    def balance(self):
        """Get current balance."""
        return self._balance

    @balance.setter
    def balance(self, value):
        """Set balance with validation."""
        if value < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = value


class User:
    """User with multiple validated properties."""

    def __init__(self, username, email, age):
        self.username = username  # Uses setter
        self.email = email  # Uses setter
        self.age = age  # Uses setter

    @property
    def username(self):
        """Get username."""
        return self._username

    @username.setter
    def username(self, value):
        """Set username - must be 3-20 chars."""
        if not isinstance(value, str):
            raise TypeError("Username must be a string")
        if not (3 <= len(value) <= 20):
            raise ValueError("Username must be 3-20 characters")
        self._username = value

    @property
    def email(self):
        """Get email."""
        return self._email

    @email.setter
    def email(self, value):
        """Set email - must contain @."""
        if "@" not in value:
            raise ValueError("Invalid email format")
        self._email = value

    @property
    def age(self):
        """Get age."""
        return self._age

    @age.setter
    def age(self, value):
        """Set age - must be 0-150."""
        if not isinstance(value, int):
            raise TypeError("Age must be an integer")
        if not (0 <= value <= 150):
            raise ValueError("Age must be between 0 and 150")
        self._age = value


def main():
    print("=== Properties with Validation ===\n")

    # Bank account validation
    print("--- Bank Account ---")
    account = BankAccount("12345", 1000)
    print(f"Initial balance: ${account.balance}")

    account.balance = 1500
    print(f"After deposit: ${account.balance}")

    try:
        account.balance = -100
    except ValueError as e:
        print(f"Error: {e}")

    # User validation
    print("\n--- User Validation ---")

    try:
        user = User("alice", "alice@example.com", 25)
        print(f"Created user: {user.username}, {user.email}, {user.age}")
    except (ValueError, TypeError) as e:
        print(f"Error: {e}")

    # Invalid username
    print("\n--- Invalid Username ---")
    try:
        user.username = "ab"  # Too short
    except ValueError as e:
        print(f"Error: {e}")

    # Invalid email
    print("\n--- Invalid Email ---")
    try:
        user.email = "not-an-email"
    except ValueError as e:
        print(f"Error: {e}")

    # Invalid age type
    print("\n--- Invalid Age Type ---")
    try:
        user.age = "25"  # String instead of int
    except TypeError as e:
        print(f"Error: {e}")

    # Invalid age range
    print("\n--- Invalid Age Range ---")
    try:
        user.age = 200
    except ValueError as e:
        print(f"Error: {e}")

    # Validation during construction
    print("\n--- Validation During Construction ---")
    try:
        invalid_user = User("a", "bad", 300)
    except (ValueError, TypeError) as e:
        print(f"Construction failed: {e}")

    print("\n=== Key Points ===")
    print("""
1. Properties enable validation on assignment
2. Validation runs even during __init__
3. Can check type, range, format, etc.
4. Raises appropriate exceptions
5. Keeps object in valid state
    """)


if __name__ == "__main__":
    main()




















































































  1. """Bank account with validation."""

    3class BankAccount:4    """Bank account with validation."""56    def __init__(self, account_number, balance=0): #?init_account7        self._account_number = account_number #?store_account_number8        self._balance = balance #?store_balance910    @property #?balance_property11    def balance(self): #?balance_getter12        """Get current balance."""13        return self._balance #?return_balance1415    @balance.setter #?balance_setter16    def balance(self, value): #?balance_setter_method17        """Set balance with validation."""18        if value < 0: #?check_negative19            raise ValueError("Balance cannot be negative") #?raise_negative_balance20        self._balance = value #?store_valid_balance212223class User:24    """User with multiple validated properties."""
  2. def main():

    72def main():73    print("=== Properties with Validation ===\n")7475    # Bank account validation #?bank_demo76    print("--- Bank Account ---")77    account = BankAccount("12345", 1000) #?create_account78    print(f"Initial balance: ${account.balance}") #?print_initial
    output=== Properties with Validation ===
    --- Bank Account ---
  3. self._account_number ← 12345, self._balance ← 1000

    6def __init__(self⟨BankAccount A⟩, account_number12345, balance1000=0): #?init_account7    self._account_number→ 12345 = account_number12345 #?store_account_number8    self._balance→ 1000 = balance1000 #?store_balance
  4. account ← ⟨BankAccount A⟩

    76print("--- Bank Account ---")77account→ ⟨BankAccount A⟩ = BankAccount("12345", 1000) #?create_account78print(f"Initial balance: ${account.balance1000}") #?print_initial
  5. def balance(self): #?balance_getter

    pass 1 of 2
    10@property #?balance_property11def balance(self⟨BankAccount A⟩): #?balance_getter12    """Get current balance."""13    return self._balance1000 #?return_balance
  6. print(f"Initial balance: ${account.balance}") #?print_initial

    77account = BankAccount("12345", 1000) #?create_account78print(f"Initial balance: ${account.balance1000}") #?print_initial7980account.balance = 1500 #?set_valid_balance81print(f"After deposit: ${account.balance}") #?print_after_deposit
    outputInitial balance: $1000
  7. self._balance ← 1500

    pass 1 of 2
    15@balance.setter #?balance_setter16def balance(self⟨BankAccount A⟩, value1500): #?balance_setter_method17    """Set balance with validation."""18    if value < 0: #?check_negative19        raise ValueError("Balance cannot be negative") #?raise_negative_balance20    self._balance→ 1500 = value1500 #?store_valid_balance
  8. account.balance ← 1500

    80account.balance→ 1500 = 1500 #?set_valid_balance81print(f"After deposit: ${account.balance1500}") #?print_after_deposit
  9. def balance(self): #?balance_getter

    pass 2 of 2
    10@property #?balance_property11def balance(self⟨BankAccount A⟩): #?balance_getter12    """Get current balance."""13    return self._balance1500 #?return_balance
  10. print(f"After deposit: ${account.balance}") #?print_after_deposit

    80account.balance = 1500 #?set_valid_balance81print(f"After deposit: ${account.balance1500}") #?print_after_deposit
    outputAfter deposit: $1500
  11. def balance(self, value): #?balance_setter_method

    pass 2 of 2
    15@balance.setter #?balance_setter16def balance(self⟨BankAccount A⟩, value-100): #?balance_setter_method17    """Set balance with validation."""18    if value < 0: #?check_negative
  12. if value < 0: #?check_negative

    17"""Set balance with validation."""18if value-100 < 0: #?check_negative19    raise ValueError("Balance cannot be negative") #?raise_negative_balance20self._balance = value #?store_valid_balance
  13. except ValueError as e: #?catch_balance_error

    84    account.balance = -100 #?set_negative_balance85except ValueError as e: #?catch_balance_error86    print(f"Error: {eBalance cannot be negative}") #?print_balance_error8788# User validation #?user_demo
    outputError: Balance cannot be negative
    Error: Balance cannot be negative
  14. print(" --- User Validation ---")

    88# User validation #?user_demo89print("\n--- User Validation ---")
    output
    --- User Validation ---
  15. def __init__(self, username, email, age): #?init_user

    pass 1 of 2
    26def __init__(self⟨User B⟩, usernamealice, emailalice@example.com, age25): #?init_user27    self.username = usernamealice  # Uses setter28    self.email = email  # Uses setter
  16. self._username ← alice, self.username ← alice

    pass 1 of 3
    26def __init__(self, username, email, age): #?init_user27    self.username→ alice = usernamealice  # Uses setter28    self.email = emailalice@example.com  # Uses setter29    self.age = age  # Uses setter3031@property #?username_property32def username(self): #?username_getter33    """Get username."""34    return self._username #?return_username3536@username.setter #?username_setter37def username(self⟨User B⟩, valuealice): #?username_setter_method38    """Set username - must be 3-20 chars."""39    if not isinstance(value, str): #?check_string40        raise TypeError("Username must be a string") #?raise_type41    if not (3 <= len(value) <= 20): #?check_length42        raise ValueError("Username must be 3-20 characters") #?raise_length43    self._username→ alice = valuealice #?store_username
    All 3 passes — pass 1 is the card above
    passselfvalueusernameemailageeself._usernameself.usernameself._emailself.email
    1⟨User B⟩alicealicealice@example.com25alicealicealice@example.comalice@example.com
    2⟨User B⟩abUsername must be 3-20 characters
    3⟨User C⟩aUsername must be 3-20 characters
  17. self._email ← alice@example.com, self.email ← alice@example.com

    pass 1 of 2
    27    self.username = username  # Uses setter28    self.email→ alice@example.com = emailalice@example.com  # Uses setter29    self.age = age25  # Uses setter3031@property #?username_property32def username(self): #?username_getter33    """Get username."""34    return self._username #?return_username3536@username.setter #?username_setter37def username(self, value): #?username_setter_method38    """Set username - must be 3-20 chars."""39    if not isinstance(value, str): #?check_string40        raise TypeError("Username must be a string") #?raise_type41    if not (3 <= len(value) <= 20): #?check_length42        raise ValueError("Username must be 3-20 characters") #?raise_length43    self._username = value #?store_username4445@property #?email_property46def email(self): #?email_getter47    """Get email."""48    return self._email #?return_email4950@email.setter #?email_setter51def email(self⟨User B⟩, valuealice@example.com): #?email_setter_method52    """Set email - must contain @."""53    if "@" not in value: #?check_at54        raise ValueError("Invalid email format") #?raise_email55    self._email→ alice@example.com = valuealice@example.com #?store_email
  18. self._age ← 25, self.age ← 25

    pass 1 of 3
    28    self.email = email  # Uses setter29    self.age→ 25 = age25  # Uses setter3031@property #?username_property32def username(self): #?username_getter33    """Get username."""34    return self._username #?return_username3536@username.setter #?username_setter37def username(self, value): #?username_setter_method38    """Set username - must be 3-20 chars."""39    if not isinstance(value, str): #?check_string40        raise TypeError("Username must be a string") #?raise_type41    if not (3 <= len(value) <= 20): #?check_length42        raise ValueError("Username must be 3-20 characters") #?raise_length43    self._username = value #?store_username4445@property #?email_property46def email(self): #?email_getter47    """Get email."""48    return self._email #?return_email4950@email.setter #?email_setter51def email(self, value): #?email_setter_method52    """Set email - must contain @."""53    if "@" not in value: #?check_at54        raise ValueError("Invalid email format") #?raise_email55    self._email = value #?store_email5657@property #?age_property58def age(self): #?age_getter59    """Get age."""60    return self._age #?return_age6162@age.setter #?age_setter63def age(self⟨User B⟩, value25): #?age_setter_method64    """Set age - must be 0-150."""65    if not isinstance(value, int): #?check_int66        raise TypeError("Age must be an integer") #?raise_age_type67    if not (0 <= value <= 150): #?check_age_range68        raise ValueError("Age must be between 0 and 150") #?raise_age_range69    self._age→ 25 = value25 #?store_age
    All 3 passes — pass 1 is the card above
    passvalueageeself._ageself.age
    125252525
    225Age must be an integer
    3200Age must be between 0 and 150
  19. user ← ⟨User B⟩

    91try: #?try_create_user92    user→ ⟨User B⟩ = User("alice", "alice@example.com", 25) #?create_valid_user93    print(f"Created user: {user.usernamealice}, {user.emailalice@example.com}, {user.age25}") #?print_user94except (ValueError, TypeError) as e: #?catch_create_error
  20. def username(self): #?username_getter

    31@property #?username_property32def username(self⟨User B⟩): #?username_getter33    """Get username."""34    return self._usernamealice #?return_username
  21. def email(self): #?email_getter

    45@property #?email_property46def email(self⟨User B⟩): #?email_getter47    """Get email."""48    return self._emailalice@example.com #?return_email
  22. def age(self): #?age_getter

    57@property #?age_property58def age(self⟨User B⟩): #?age_getter59    """Get age."""60    return self._age25 #?return_age
  23. print(f"Created user: {user.username}, {user.email}, {user.age}") #?pr…

    92    user = User("alice", "alice@example.com", 25) #?create_valid_user93    print(f"Created user: {user.usernamealice}, {user.emailalice@example.com}, {user.age25}") #?print_user94except (ValueError, TypeError) as e: #?catch_create_error
    outputCreated user: alice, alice@example.com, 25
  24. print(" --- Invalid Username ---")

    97# Invalid username #?invalid_username98print("\n--- Invalid Username ---")99try: #?try_invalid_username
    output
    --- Invalid Username ---
  25. if not (3 <= len(value) <= 20): #?check_length

    pass 1 of 2
    40    raise TypeError("Username must be a string") #?raise_type41if not (3 <= len(valueab) <= 20): #?check_length42    raise ValueError("Username must be 3-20 characters") #?raise_length43self._username = value #?store_username
  26. except ValueError as e: #?catch_username_error

    100    user.username = "ab"  # Too short101except ValueError as e: #?catch_username_error102    print(f"Error: {eUsername must be 3-20 characters}") #?print_username_error103104# Invalid email #?invalid_email
    outputError: Username must be 3-20 characters
    Error: Username must be 3-20 characters
  27. print(" --- Invalid Email ---")

    104# Invalid email #?invalid_email105print("\n--- Invalid Email ---")106try: #?try_invalid_email
    output
    --- Invalid Email ---
  28. def email(self, value): #?email_setter_method

    pass 2 of 2
    50@email.setter #?email_setter51def email(self⟨User B⟩, valuenot-an-email): #?email_setter_method52    """Set email - must contain @."""53    if "@" not in value: #?check_at
  29. if "@" not in value: #?check_at

    52"""Set email - must contain @."""53if "@" not in valuenot-an-email: #?check_at54    raise ValueError("Invalid email format") #?raise_email55self._email = value #?store_email
  30. except ValueError as e: #?catch_email_error

    107    user.email = "not-an-email" #?set_invalid_email108except ValueError as e: #?catch_email_error109    print(f"Error: {eInvalid email format}") #?print_email_error110111# Invalid age type #?invalid_age_type
    outputError: Invalid email format
    Error: Invalid email format
  31. print(" --- Invalid Age Type ---")

    111# Invalid age type #?invalid_age_type112print("\n--- Invalid Age Type ---")113try: #?try_invalid_age_type
    output
    --- Invalid Age Type ---
  32. if not isinstance(value, int): #?check_int

    64"""Set age - must be 0-150."""65if not isinstance(value25, int): #?check_int66    raise TypeError("Age must be an integer") #?raise_age_type67if not (0 <= value <= 150): #?check_age_range
  33. except TypeError as e: #?catch_age_type_error

    114    user.age = "25"  # String instead of int115except TypeError as e: #?catch_age_type_error116    print(f"Error: {eAge must be an integer}") #?print_age_type_error117118# Invalid age range #?invalid_age_range
    outputError: Age must be an integer
    Error: Age must be an integer
  34. print(" --- Invalid Age Range ---")

    118# Invalid age range #?invalid_age_range119print("\n--- Invalid Age Range ---")120try: #?try_invalid_age_range
    output
    --- Invalid Age Range ---
  35. if not (0 <= value <= 150): #?check_age_range

    66    raise TypeError("Age must be an integer") #?raise_age_type67if not (0 <= value200 <= 150): #?check_age_range68    raise ValueError("Age must be between 0 and 150") #?raise_age_range69self._age = value #?store_age
  36. except ValueError as e: #?catch_age_range_error

    121    user.age = 200 #?set_out_of_range_age122except ValueError as e: #?catch_age_range_error123    print(f"Error: {eAge must be between 0 and 150}") #?print_age_range_error124125# Validation during construction #?construction_validation
    outputError: Age must be between 0 and 150
    Error: Age must be between 0 and 150
  37. print(" --- Validation During Construction ---")

    125# Validation during construction #?construction_validation126print("\n--- Validation During Construction ---")127try: #?try_invalid_construction
    output
    --- Validation During Construction ---
  38. def __init__(self, username, email, age): #?init_user

    pass 2 of 2
    26def __init__(self⟨User C⟩, usernamea, emailbad, age300): #?init_user27    self.username = usernamea  # Uses setter28    self.email = email  # Uses setter
  39. if not (3 <= len(value) <= 20): #?check_length

    pass 2 of 2
    40    raise TypeError("Username must be a string") #?raise_type41if not (3 <= len(valuea) <= 20): #?check_length42    raise ValueError("Username must be 3-20 characters") #?raise_length43self._username = value #?store_username
  40. except (ValueError, TypeError) as e: #?catch_construction_error

    128    invalid_user = User("a", "bad", 300) #?create_invalid_user129except (ValueError, TypeError) as e: #?catch_construction_error130    print(f"Construction failed: {eUsername must be 3-20 characters}") #?print_construction_error131132print("\n=== Key Points ===")
    outputConstruction failed: Username must be 3-20 characters
    Construction failed: Username must be 3-20 characters
  41. print(" === Key Points ===")

    132    print("\n=== Key Points ===")133    print("""1341. Properties enable validation on assignment1352. Validation runs even during __init__1363. Can check type, range, format, etc.1374. Raises appropriate exceptions1385. Keeps object in valid state139    """)
    output
    === Key Points ===
    
    1. Properties enable validation on assignment
    2. Validation runs even during __init__
    3. Can check type, range, format, etc.
    4. Raises appropriate exceptions
    5. Keeps object in valid state
        
  42. main()

    142if __name__ == "__main__":143    main()

@name.setter defines setter. Validate and raise exception if invalid.

Computed properties

Calculate values on demand.

computed.py
Replay: real traced execution (multi-file project)
# Computed Properties

import math


class Circle:
    """Circle with computed properties."""

    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        """Get radius."""
        return self._radius

    @radius.setter
    def radius(self, value):
        """Set radius."""
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value

    @property
    def diameter(self):
        """Computed: diameter = 2 * radius."""
        return 2 * self._radius

    @diameter.setter
    def diameter(self, value):
        """Set diameter (updates radius)."""
        self.radius = value / 2  # Uses radius setter

    @property
    def area(self):
        """Computed: area = π * r²."""
        return math.pi * self._radius ** 2

    @property
    def circumference(self):
        """Computed: circumference = 2 * π * r."""
        return 2 * math.pi * self._radius


class Temperature:
    """Temperature with Celsius/Fahrenheit conversion."""

    def __init__(self, celsius=0):
        self._celsius = celsius

    @property
    def celsius(self):
        """Get temperature in Celsius."""
        return self._celsius

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

    @property
    def fahrenheit(self):
        """Computed: convert to Fahrenheit."""
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        """Set temperature in Fahrenheit (updates celsius)."""
        self.celsius = (value - 32) * 5/9

    @property
    def kelvin(self):
        """Computed: convert to Kelvin."""
        return self._celsius + 273.15

    @kelvin.setter
    def kelvin(self, value):
        """Set temperature in Kelvin (updates celsius)."""
        self.celsius = value - 273.15


class Rectangle:
    """Rectangle with computed properties."""

    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: area = width * height."""
        return self._width * self._height

    @property
    def perimeter(self):
        """Computed: perimeter = 2 * (width + height)."""
        return 2 * (self._width + self._height)

    @property
    def aspect_ratio(self):
        """Computed: aspect ratio = width / height."""
        return self._width / self._height

    @property
    def is_square(self):
        """Computed: check if it's a square."""
        return self._width == self._height


def main():
    print("=== Computed Properties ===\n")

    # Circle with computed properties
    print("--- Circle ---")
    circle = Circle(5)
    print(f"Radius: {circle.radius}")
    print(f"Diameter: {circle.diameter}")
    print(f"Area: {circle.area:.2f}")
    print(f"Circumference: {circle.circumference:.2f}")

    # Update via diameter
    print("\n--- Update via Diameter ---")
    circle.diameter = 20
    print(f"New radius: {circle.radius}")
    print(f"New area: {circle.area:.2f}")

    # Temperature conversions
    print("\n--- Temperature Conversions ---")
    temp = Temperature(25)
    print(f"Celsius: {temp.celsius}°C")
    print(f"Fahrenheit: {temp.fahrenheit}°F")
    print(f"Kelvin: {temp.kelvin}K")

    # Set via Fahrenheit
    print("\n--- Set via Fahrenheit ---")
    temp.fahrenheit = 212
    print(f"Celsius: {temp.celsius}°C")
    print(f"Kelvin: {temp.kelvin}K")

    # Rectangle computed properties
    print("\n--- Rectangle ---")
    rect = Rectangle(10, 5)
    print(f"Dimensions: {rect.width} x {rect.height}")
    print(f"Area: {rect.area}")
    print(f"Perimeter: {rect.perimeter}")
    print(f"Aspect ratio: {rect.aspect_ratio}")
    print(f"Is square: {rect.is_square}")

    # Make it a square
    print("\n--- Make it a Square ---")
    rect.width = 5
    print(f"Dimensions: {rect.width} x {rect.height}")
    print(f"Is square: {rect.is_square}")

    print("\n=== Key Points ===")
    print("""
1. Computed properties calculate values on-the-fly
2. No need to store derived values
3. Always consistent with source data
4. Can have setters that update source data
5. Useful for conversions, calculations, flags
    """)


if __name__ == "__main__":
    main()


# import_math
# import math
#
# For π and other math functions.
#

# init_circle
# def __init__(self, radius):
#
# Store only radius, compute rest.
#

# store_radius
# self._radius = radius
#
# Single source of truth.
#

# radius_property
# @property
#
# Radius getter.
#

# radius_getter
# def radius(self):
#
# Get radius.
#

# return_radius
# return self._radius
#
# Return stored radius.
#

# radius_setter
# @radius.setter
#
# Radius setter with validation.
#

# radius_setter_method
# def radius(self, value):
#
# Set radius.
#

# check_positive
# if value <= 0:
#
# Must be positive.
#

# raise_radius
# raise ValueError("Radius must be positive")
#
# Validation error.
#

# store_valid_radius
# self._radius = value
#
# Store valid radius.
#

# diameter_property
# @property
#
# Computed diameter.
#

# diameter_getter
# def diameter(self):
#
# Calculated from radius.
#

# calc_diameter
# return 2 * self._radius
#
# diameter = 2r
#

# diameter_setter
# @diameter.setter
#
# Can set diameter too.
#

# diameter_setter_method
# def diameter(self, value):
#
# Set diameter.
#

# set_radius_from_diameter
# self.radius = value / 2
#
# Updates radius (uses radius setter).
#

# area_property
# @property
#
# Computed area.
#

# area_getter
# def area(self):
#
# Calculate area.
#

# calc_area
# return math.pi * self._radius ** 2
#
# area = pi * r^2
#

# circumference_property
# @property
#
# Computed circumference.
#

# circumference_getter
# def circumference(self):
#
# Calculate circumference.
#

# calc_circumference
# return 2 * math.pi * self._radius
#
# circumference = 2 * pi * r
#

# init_temp
# def __init__(self, celsius=0):
#
# Store in Celsius.
#

# store_celsius
# self._celsius = celsius
#
# Single source (Celsius).
#

# celsius_property
# @property
#
# Celsius getter.
#

# celsius_getter
# def celsius(self):
#
# Get Celsius.
#

# return_celsius
# return self._celsius
#
# Return stored value.
#

# celsius_setter
# @celsius.setter
#
# Celsius setter.
#

# celsius_setter_method
# def celsius(self, value):
#
# Set Celsius.
#

# check_absolute_zero
# if value < -273.15:
#
# Check absolute zero.
#

# raise_absolute_zero
# raise ValueError("Below absolute zero!")
#
# Can't go below -273.15°C.
#

# store_valid_celsius
# self._celsius = value
#
# Store valid Celsius.
#

# fahrenheit_property
# @property
#
# Computed Fahrenheit.
#

# fahrenheit_getter
# def fahrenheit(self):
#
# Convert to Fahrenheit.
#

# calc_fahrenheit
# return self._celsius * 9/5 + 32
#
# F = C x 9/5 + 32
#

# fahrenheit_setter
# @fahrenheit.setter
#
# Can set via Fahrenheit.
#

# fahrenheit_setter_method
# def fahrenheit(self, value):
#
# Set Fahrenheit.
#

# set_celsius_from_fahrenheit
# self.celsius = (value - 32) * 5/9
#
# Convert to Celsius and set.
# C = (F - 32) x 5/9
#

# kelvin_property
# @property
#
# Computed Kelvin.
#

# kelvin_getter
# def kelvin(self):
#
# Convert to Kelvin.
#

# calc_kelvin
# return self._celsius + 273.15
#
# K = C + 273.15
#

# kelvin_setter
# @kelvin.setter
#
# Can set via Kelvin.
#

# kelvin_setter_method
# def kelvin(self, value):
#
# Set Kelvin.
#

# set_celsius_from_kelvin
# self.celsius = value - 273.15
#
# Convert to Celsius.
# C = K - 273.15
#

# init_rectangle
# def __init__(self, width, height):
#
# Store width and height.
#

# store_width
# self._width = width
#
# Store width.
#

# store_height
# self._height = height
#
# Store height.
#

# width_property
# @property
#
# Width getter.
#

# width_getter
# def width(self):
#
# Get width.
#

# return_width
# return self._width
#
# Return width.
#

# width_setter
# @width.setter
#
# Width setter.
#

# width_setter_method
# def width(self, value):
#
# Set width.
#

# check_width
# if value <= 0:
#
# Validate positive.
#

# raise_width
# raise ValueError("Width must be positive")
#
# Width error.
#

# store_valid_width
# self._width = value
#
# Store valid width.
#

# height_property
# @property
#
# Height getter.
#

# height_getter
# def height(self):
#
# Get height.
#

# return_height
# return self._height
#
# Return height.
#

# height_setter
# @height.setter
#
# Height setter.
#

# height_setter_method
# def height(self, value):
#
# Set height.
#

# check_height
# if value <= 0:
#
# Validate positive.
#

# raise_height
# raise ValueError("Height must be positive")
#
# Height error.
#

# store_valid_height
# self._height = value
#
# Store valid height.
#

# area_rect_property
# @property
#
# Computed area.
#

# area_rect_getter
# def area(self):
#
# Calculate area.
#

# calc_rect_area
# return self._width * self._height
#
# area = width x height
#

# perimeter_property
# @property
#
# Computed perimeter.
#

# perimeter_getter
# def perimeter(self):
#
# Calculate perimeter.
#

# calc_perimeter
# return 2 * (self._width + self._height)
#
# perimeter = 2(w + h)
#

# aspect_ratio_property
# @property
#
# Computed aspect ratio.
#

# aspect_ratio_getter
# def aspect_ratio(self):
#
# Calculate aspect ratio.
#

# calc_aspect_ratio
# return self._width / self._height
#
# ratio = width / height
#

# is_square_property
# @property
#
# Boolean computed property.
#

# is_square_getter
# def is_square(self):
#
# Check if square.
#

# check_square
# return self._width == self._height
#
# True if width == height.
#

# circle_demo
# Circle with computed properties
#
# Demonstrate computed properties.
#

# create_circle
# circle = Circle(5)
#
# Radius = 5
#

# print_radius
# print(f"Radius: {circle.radius}")
#
# "Radius: 5"
#

# print_diameter
# print(f"Diameter: {circle.diameter}")
#
# "Diameter: 10" (computed: 2 x 5)
#

# print_area
# print(f"Area: {circle.area:.2f}")
#
# "Area: 78.54" (computed: pi * 5^2)
#

# print_circumference
# print(f"Circumference: {circle.circumference:.2f}")
#
# "Circumference: 31.42" (computed: 2*pi * 5)
#

# update_diameter
# Update via diameter
#
# Set diameter, radius updates.
#

# set_diameter
# circle.diameter = 20
#
# Sets radius to 10.
#

# print_new_radius
# print(f"New radius: {circle.radius}")
#
# "New radius: 10"
#

# print_new_area
# print(f"New area: {circle.area:.2f}")
#
# "New area: 314.16" (pi * 10^2)
#

# temp_demo
# Temperature conversions
#
# Multiple unit support.
#

# create_temp
# temp = Temperature(25)
#
# 25°C
#

# print_celsius
# print(f"Celsius: {temp.celsius}°C")
#
# "Celsius: 25°C"
#

# print_fahrenheit
# print(f"Fahrenheit: {temp.fahrenheit}°F")
#
# "Fahrenheit: 77.0°F" (computed)
#

# print_kelvin
# print(f"Kelvin: {temp.kelvin}K")
#
# "Kelvin: 298.15K" (computed)
#

# set_fahrenheit
# Set via Fahrenheit
#
# Set in different unit.
#

# set_boiling
# temp.fahrenheit = 212
#
# Boiling point of water.
#

# print_boiling_celsius
# print(f"Celsius: {temp.celsius}°C")
#
# "Celsius: 100.0°C"
#

# print_boiling_kelvin
# print(f"Kelvin: {temp.kelvin}K")
#
# "Kelvin: 373.15K"
#

# rectangle_demo
# Rectangle computed properties
#
# Multiple computed properties.
#

# create_rect
# rect = Rectangle(10, 5)
#
# 10 x 5 rectangle.
#

# print_dimensions
# print(f"Dimensions: {rect.width} x {rect.height}")
#
# "Dimensions: 10 x 5"
#

# print_rect_area
# print(f"Area: {rect.area}")
#
# "Area: 50"
#

# print_perimeter
# print(f"Perimeter: {rect.perimeter}")
#
# "Perimeter: 30"
#

# print_aspect_ratio
# print(f"Aspect ratio: {rect.aspect_ratio}")
#
# "Aspect ratio: 2.0"
#

# print_is_square
# print(f"Is square: {rect.is_square}")
#
# "Is square: False"
#

# make_square
# Make it a square
#
# Update to square.
#

# set_width_5
# rect.width = 5
#
# Now 5 x 5.
#

# print_square_dimensions
# print(f"Dimensions: {rect.width} x {rect.height}")
#
# "Dimensions: 5 x 5"
#

# print_is_now_square
# print(f"Is square: {rect.is_square}")
#
# "Is square: True"
#
  1. """Circle with computed properties."""

    6class Circle:7    """Circle with computed properties."""89    def __init__(self, radius): #?init_circle10        self._radius = radius #?store_radius1112    @property #?radius_property13    def radius(self): #?radius_getter14        """Get radius."""15        return self._radius #?return_radius1617    @radius.setter #?radius_setter18    def radius(self, value): #?radius_setter_method19        """Set radius."""20        if value <= 0: #?check_positive21            raise ValueError("Radius must be positive") #?raise_radius22        self._radius = value #?store_valid_radius2324    @property #?diameter_property25    def diameter(self): #?diameter_getter26        """Computed: diameter = 2 * radius."""27        return 2 * self._radius #?calc_diameter2829    @diameter.setter #?diameter_setter30    def diameter(self, value): #?diameter_setter_method31        """Set diameter (updates radius)."""32        self.radius = value / 2  # Uses radius setter3334    @property #?area_property35    def area(self): #?area_getter36        """Computed: area = π * r²."""37        return math.pi * self._radius ** 2 #?calc_area3839    @property #?circumference_property40    def circumference(self): #?circumference_getter41        """Computed: circumference = 2 * π * r."""42        return 2 * math.pi * self._radius #?calc_circumference434445class Temperature:46    """Temperature with Celsius/Fahrenheit conversion."""4748    def __init__(self, celsius=0): #?init_temp49        self._celsius = celsius #?store_celsius5051    @property #?celsius_property52    def celsius(self): #?celsius_getter53        """Get temperature in Celsius."""54        return self._celsius #?return_celsius5556    @celsius.setter #?celsius_setter57    def celsius(self, value): #?celsius_setter_method58        """Set temperature in Celsius."""59        if value < -273.15: #?check_absolute_zero60            raise ValueError("Below absolute zero!") #?raise_absolute_zero61        self._celsius = value #?store_valid_celsius6263    @property #?fahrenheit_property64    def fahrenheit(self): #?fahrenheit_getter65        """Computed: convert to Fahrenheit."""66        return self._celsius * 9/5 + 32 #?calc_fahrenheit6768    @fahrenheit.setter #?fahrenheit_setter69    def fahrenheit(self, value): #?fahrenheit_setter_method70        """Set temperature in Fahrenheit (updates celsius)."""71        self.celsius = (value - 32) * 5/9 #?set_celsius_from_fahrenheit7273    @property #?kelvin_property74    def kelvin(self): #?kelvin_getter75        """Computed: convert to Kelvin."""76        return self._celsius + 273.15 #?calc_kelvin7778    @kelvin.setter #?kelvin_setter79    def kelvin(self, value): #?kelvin_setter_method80        """Set temperature in Kelvin (updates celsius)."""81        self.celsius = value - 273.15 #?set_celsius_from_kelvin828384class Rectangle:85    """Rectangle with computed properties."""
  2. def main():

    132def main():133    print("=== Computed Properties ===\n")134135    # Circle with computed properties #?circle_demo136    print("--- Circle ---")137    circle = Circle(5) #?create_circle138    print(f"Radius: {circle.radius}") #?print_radius
    output=== Computed Properties ===
    --- Circle ---
  3. self._radius ← 5

    9def __init__(self⟨Circle A⟩, radius5): #?init_circle10    self._radius→ 5 = radius5 #?store_radius
  4. circle ← ⟨Circle A⟩

    136print("--- Circle ---")137circle→ ⟨Circle A⟩ = Circle(5) #?create_circle138print(f"Radius: {circle.radius5}") #?print_radius139print(f"Diameter: {circle.diameter}") #?print_diameter
  5. def radius(self): #?radius_getter

    pass 1 of 2
    12@property #?radius_property13def radius(self⟨Circle A⟩): #?radius_getter14    """Get radius."""15    return self._radius5 #?return_radius
  6. print(f"Radius: {circle.radius}") #?print_radius

    137circle = Circle(5) #?create_circle138print(f"Radius: {circle.radius5}") #?print_radius139print(f"Diameter: {circle.diameter10}") #?print_diameter140print(f"Area: {circle.area:.2f}") #?print_area
    outputRadius: 5
  7. def diameter(self): #?diameter_getter

    24@property #?diameter_property25def diameter(self⟨Circle A⟩): #?diameter_getter26    """Computed: diameter = 2 * radius."""27    return 2 * self._radius5 #?calc_diameter
  8. print(f"Diameter: {circle.diameter}") #?print_diameter

    138print(f"Radius: {circle.radius}") #?print_radius139print(f"Diameter: {circle.diameter10}") #?print_diameter140print(f"Area: {circle.area78.53981633974483:.2f}") #?print_area141print(f"Circumference: {circle.circumference:.2f}") #?print_circumference
    outputDiameter: 10
  9. def area(self): #?area_getter

    pass 1 of 2
    34@property #?area_property35def area(self⟨Circle A⟩): #?area_getter36    """Computed: area = π * r²."""37    return math.pi3.141592653589793 * self._radius5 ** 2 #?calc_area
  10. print(f"Area: {circle.area:.2f}") #?print_area

    139print(f"Diameter: {circle.diameter}") #?print_diameter140print(f"Area: {circle.area78.53981633974483:.2f}") #?print_area141print(f"Circumference: {circle.circumference31.41592653589793:.2f}") #?print_circumference
    outputArea: 78.54
  11. def circumference(self): #?circumference_getter

    39@property #?circumference_property40def circumference(self⟨Circle A⟩): #?circumference_getter41    """Computed: circumference = 2 * π * r."""42    return 2 * math.pi3.141592653589793 * self._radius5 #?calc_circumference
  12. print(f"Circumference: {circle.circumference:.2f}") #?print_circumfere…

    140print(f"Area: {circle.area:.2f}") #?print_area141print(f"Circumference: {circle.circumference31.41592653589793:.2f}") #?print_circumference142143# Update via diameter #?update_diameter144print("\n--- Update via Diameter ---")145circle.diameter = 20 #?set_diameter146print(f"New radius: {circle.radius}") #?print_new_radius
    outputCircumference: 31.42
    
    --- Update via Diameter ---
  13. def diameter(self, value): #?diameter_setter_method

    29@diameter.setter #?diameter_setter30def diameter(self⟨Circle A⟩, value20): #?diameter_setter_method31    """Set diameter (updates radius)."""32    self.radius = value20 / 2  # Uses radius setter
  14. self._radius ← 10.0

    17@radius.setter #?radius_setter18def radius(self⟨Circle A⟩, value10.0): #?radius_setter_method19    """Set radius."""20    if value <= 0: #?check_positive21        raise ValueError("Radius must be positive") #?raise_radius22    self._radius→ 10.0 = value10.0 #?store_valid_radius
  15. self.radius ← 10.0

    31"""Set diameter (updates radius)."""32self.radius→ 10.0 = value20 / 2  # Uses radius setter
  16. circle.diameter ← 20.0

    144print("\n--- Update via Diameter ---")145circle.diameter→ 20.0 = 20 #?set_diameter146print(f"New radius: {circle.radius10.0}") #?print_new_radius147print(f"New area: {circle.area:.2f}") #?print_new_area
  17. def radius(self): #?radius_getter

    pass 2 of 2
    12@property #?radius_property13def radius(self⟨Circle A⟩): #?radius_getter14    """Get radius."""15    return self._radius10.0 #?return_radius
  18. print(f"New radius: {circle.radius}") #?print_new_radius

    145circle.diameter = 20 #?set_diameter146print(f"New radius: {circle.radius10.0}") #?print_new_radius147print(f"New area: {circle.area314.1592653589793:.2f}") #?print_new_area
    outputNew radius: 10.0
  19. def area(self): #?area_getter

    pass 2 of 2
    34@property #?area_property35def area(self⟨Circle A⟩): #?area_getter36    """Computed: area = π * r²."""37    return math.pi3.141592653589793 * self._radius10.0 ** 2 #?calc_area
  20. print(f"New area: {circle.area:.2f}") #?print_new_area

    146print(f"New radius: {circle.radius}") #?print_new_radius147print(f"New area: {circle.area314.1592653589793:.2f}") #?print_new_area148149# Temperature conversions #?temp_demo150print("\n--- Temperature Conversions ---")151temp = Temperature(25) #?create_temp152print(f"Celsius: {temp.celsius}°C") #?print_celsius
    outputNew area: 314.16
    
    --- Temperature Conversions ---
  21. self._celsius ← 25

    48def __init__(self⟨Temperature B⟩, celsius25=0): #?init_temp49    self._celsius→ 25 = celsius25 #?store_celsius
  22. temp ← ⟨Temperature B⟩

    150print("\n--- Temperature Conversions ---")151temp→ ⟨Temperature B⟩ = Temperature(25) #?create_temp152print(f"Celsius: {temp.celsius25}°C") #?print_celsius153print(f"Fahrenheit: {temp.fahrenheit}°F") #?print_fahrenheit
  23. def celsius(self): #?celsius_getter

    pass 1 of 2
    51@property #?celsius_property52def celsius(self⟨Temperature B⟩): #?celsius_getter53    """Get temperature in Celsius."""54    return self._celsius25 #?return_celsius
  24. print(f"Celsius: {temp.celsius}°C") #?print_celsius

    151temp = Temperature(25) #?create_temp152print(f"Celsius: {temp.celsius25}°C") #?print_celsius153print(f"Fahrenheit: {temp.fahrenheit77.0}°F") #?print_fahrenheit154print(f"Kelvin: {temp.kelvin}K") #?print_kelvin
    outputCelsius: 25°C
  25. def fahrenheit(self): #?fahrenheit_getter

    63@property #?fahrenheit_property64def fahrenheit(self⟨Temperature B⟩): #?fahrenheit_getter65    """Computed: convert to Fahrenheit."""66    return self._celsius25 * 9/5 + 32 #?calc_fahrenheit
  26. print(f"Fahrenheit: {temp.fahrenheit}°F") #?print_fahrenheit

    152print(f"Celsius: {temp.celsius}°C") #?print_celsius153print(f"Fahrenheit: {temp.fahrenheit77.0}°F") #?print_fahrenheit154print(f"Kelvin: {temp.kelvin298.15}K") #?print_kelvin
    outputFahrenheit: 77.0°F
  27. def kelvin(self): #?kelvin_getter

    pass 1 of 2
    73@property #?kelvin_property74def kelvin(self⟨Temperature B⟩): #?kelvin_getter75    """Computed: convert to Kelvin."""76    return self._celsius25 + 273.15 #?calc_kelvin
  28. print(f"Kelvin: {temp.kelvin}K") #?print_kelvin

    153print(f"Fahrenheit: {temp.fahrenheit}°F") #?print_fahrenheit154print(f"Kelvin: {temp.kelvin298.15}K") #?print_kelvin155156# Set via Fahrenheit #?set_fahrenheit157print("\n--- Set via Fahrenheit ---")158temp.fahrenheit = 212 #?set_boiling159print(f"Celsius: {temp.celsius}°C") #?print_boiling_celsius
    outputKelvin: 298.15K
    
    --- Set via Fahrenheit ---
  29. def fahrenheit(self, value): #?fahrenheit_setter_method

    68@fahrenheit.setter #?fahrenheit_setter69def fahrenheit(self⟨Temperature B⟩, value212): #?fahrenheit_setter_method70    """Set temperature in Fahrenheit (updates celsius)."""71    self.celsius = (value212 - 32) * 5/9 #?set_celsius_from_fahrenheit
  30. self._celsius ← 100.0

    56@celsius.setter #?celsius_setter57def celsius(self⟨Temperature B⟩, value100.0): #?celsius_setter_method58    """Set temperature in Celsius."""59    if value < -273.15: #?check_absolute_zero60        raise ValueError("Below absolute zero!") #?raise_absolute_zero61    self._celsius→ 100.0 = value100.0 #?store_valid_celsius
  31. self.celsius ← 100.0

    70"""Set temperature in Fahrenheit (updates celsius)."""71self.celsius→ 100.0 = (value212 - 32) * 5/9 #?set_celsius_from_fahrenheit
  32. temp.fahrenheit ← 212.0

    157print("\n--- Set via Fahrenheit ---")158temp.fahrenheit→ 212.0 = 212 #?set_boiling159print(f"Celsius: {temp.celsius100.0}°C") #?print_boiling_celsius160print(f"Kelvin: {temp.kelvin}K") #?print_boiling_kelvin
  33. def celsius(self): #?celsius_getter

    pass 2 of 2
    51@property #?celsius_property52def celsius(self⟨Temperature B⟩): #?celsius_getter53    """Get temperature in Celsius."""54    return self._celsius100.0 #?return_celsius
  34. print(f"Celsius: {temp.celsius}°C") #?print_boiling_celsius

    158temp.fahrenheit = 212 #?set_boiling159print(f"Celsius: {temp.celsius100.0}°C") #?print_boiling_celsius160print(f"Kelvin: {temp.kelvin373.15}K") #?print_boiling_kelvin
    outputCelsius: 100.0°C
  35. def kelvin(self): #?kelvin_getter

    pass 2 of 2
    73@property #?kelvin_property74def kelvin(self⟨Temperature B⟩): #?kelvin_getter75    """Computed: convert to Kelvin."""76    return self._celsius100.0 + 273.15 #?calc_kelvin
  36. print(f"Kelvin: {temp.kelvin}K") #?print_boiling_kelvin

    159print(f"Celsius: {temp.celsius}°C") #?print_boiling_celsius160print(f"Kelvin: {temp.kelvin373.15}K") #?print_boiling_kelvin161162# Rectangle computed properties #?rectangle_demo163print("\n--- Rectangle ---")164rect = Rectangle(10, 5) #?create_rect165print(f"Dimensions: {rect.width} x {rect.height}") #?print_dimensions
    outputKelvin: 373.15K
    
    --- Rectangle ---
  37. self._width ← 10, self._height ← 5

    87def __init__(self⟨Rectangle C⟩, width10, height5): #?init_rectangle88    self._width→ 10 = width10 #?store_width89    self._height→ 5 = height5 #?store_height
  38. rect ← ⟨Rectangle C⟩

    163print("\n--- Rectangle ---")164rect→ ⟨Rectangle C⟩ = Rectangle(10, 5) #?create_rect165print(f"Dimensions: {rect.width10} x {rect.height5}") #?print_dimensions166print(f"Area: {rect.area}") #?print_rect_area
  39. def width(self): #?width_getter

    pass 1 of 2
    91@property #?width_property92def width(self⟨Rectangle C⟩): #?width_getter93    return self._width10 #?return_width
  40. def height(self): #?height_getter

    pass 1 of 2
    101@property #?height_property102def height(self⟨Rectangle C⟩): #?height_getter103    return self._height5 #?return_height
  41. print(f"Dimensions: {rect.width} x {rect.height}") #?print_dimensions

    164rect = Rectangle(10, 5) #?create_rect165print(f"Dimensions: {rect.width10} x {rect.height5}") #?print_dimensions166print(f"Area: {rect.area50}") #?print_rect_area167print(f"Perimeter: {rect.perimeter}") #?print_perimeter
    outputDimensions: 10 x 5
  42. def area(self): #?area_rect_getter

    111@property #?area_rect_property112def area(self⟨Rectangle C⟩): #?area_rect_getter113    """Computed: area = width * height."""114    return self._width10 * self._height5 #?calc_rect_area
  43. print(f"Area: {rect.area}") #?print_rect_area

    165print(f"Dimensions: {rect.width} x {rect.height}") #?print_dimensions166print(f"Area: {rect.area50}") #?print_rect_area167print(f"Perimeter: {rect.perimeter30}") #?print_perimeter168print(f"Aspect ratio: {rect.aspect_ratio}") #?print_aspect_ratio
    outputArea: 50
  44. def perimeter(self): #?perimeter_getter

    116@property #?perimeter_property117def perimeter(self⟨Rectangle C⟩): #?perimeter_getter118    """Computed: perimeter = 2 * (width + height)."""119    return 2 * (self._width10 + self._height5) #?calc_perimeter
  45. print(f"Perimeter: {rect.perimeter}") #?print_perimeter

    166print(f"Area: {rect.area}") #?print_rect_area167print(f"Perimeter: {rect.perimeter30}") #?print_perimeter168print(f"Aspect ratio: {rect.aspect_ratio2.0}") #?print_aspect_ratio169print(f"Is square: {rect.is_square}") #?print_is_square
    outputPerimeter: 30
  46. def aspect_ratio(self): #?aspect_ratio_getter

    121@property #?aspect_ratio_property122def aspect_ratio(self⟨Rectangle C⟩): #?aspect_ratio_getter123    """Computed: aspect ratio = width / height."""124    return self._width10 / self._height5 #?calc_aspect_ratio
  47. print(f"Aspect ratio: {rect.aspect_ratio}") #?print_aspect_ratio

    167print(f"Perimeter: {rect.perimeter}") #?print_perimeter168print(f"Aspect ratio: {rect.aspect_ratio2.0}") #?print_aspect_ratio169print(f"Is square: {rect.is_squareFalse}") #?print_is_square
    outputAspect ratio: 2.0
  48. def is_square(self): #?is_square_getter

    pass 1 of 2
    126@property #?is_square_property127def is_square(self⟨Rectangle C⟩): #?is_square_getter128    """Computed: check if it's a square."""129    return self._width10 == self._height5 #?check_square
  49. print(f"Is square: {rect.is_square}") #?print_is_square

    168print(f"Aspect ratio: {rect.aspect_ratio}") #?print_aspect_ratio169print(f"Is square: {rect.is_squareFalse}") #?print_is_square170171# Make it a square #?make_square172print("\n--- Make it a Square ---")173rect.width = 5 #?set_width_5174print(f"Dimensions: {rect.width} x {rect.height}") #?print_square_dimensions
    outputIs square: False
    
    --- Make it a Square ---
  50. self._width ← 5

    95@width.setter #?width_setter96def width(self⟨Rectangle C⟩, value5): #?width_setter_method97    if value <= 0: #?check_width98        raise ValueError("Width must be positive") #?raise_width99    self._width→ 5 = value5 #?store_valid_width
  51. rect.width ← 5

    172print("\n--- Make it a Square ---")173rect.width→ 5 = 5 #?set_width_5174print(f"Dimensions: {rect.width5} x {rect.height5}") #?print_square_dimensions175print(f"Is square: {rect.is_square}") #?print_is_now_square
  52. def width(self): #?width_getter

    pass 2 of 2
    91@property #?width_property92def width(self⟨Rectangle C⟩): #?width_getter93    return self._width5 #?return_width
  53. def height(self): #?height_getter

    pass 2 of 2
    101@property #?height_property102def height(self⟨Rectangle C⟩): #?height_getter103    return self._height5 #?return_height
  54. print(f"Dimensions: {rect.width} x {rect.height}") #?print_square_dime…

    173rect.width = 5 #?set_width_5174print(f"Dimensions: {rect.width5} x {rect.height5}") #?print_square_dimensions175print(f"Is square: {rect.is_squareTrue}") #?print_is_now_square
    outputDimensions: 5 x 5
  55. def is_square(self): #?is_square_getter

    pass 2 of 2
    126@property #?is_square_property127def is_square(self⟨Rectangle C⟩): #?is_square_getter128    """Computed: check if it's a square."""129    return self._width5 == self._height5 #?check_square
  56. print(f"Is square: {rect.is_square}") #?print_is_now_square

    174    print(f"Dimensions: {rect.width} x {rect.height}") #?print_square_dimensions175    print(f"Is square: {rect.is_squareTrue}") #?print_is_now_square176177    print("\n=== Key Points ===")178    print("""1791. Computed properties calculate values on-the-fly1802. No need to store derived values1813. Always consistent with source data1824. Can have setters that update source data1835. Useful for conversions, calculations, flags184    """)
    outputIs square: True
    
    === Key Points ===
    
    1. Computed properties calculate values on-the-fly
    2. No need to store derived values
    3. Always consistent with source data
    4. Can have setters that update source data
    5. Useful for conversions, calculations, flags
        
  57. main()

    187if __name__ == "__main__":188    main()

area computed from radius. Always up-to-date, no storage needed.

computed property Value calculated from other attributes. No stored copy to get stale.

Read-only properties

Prevent modification after creation.

readonly.py
Replay: real traced execution (multi-file project)
# Read-only Properties

from datetime import datetime


class Person:
    """Person with read-only birth_year."""

    def __init__(self, name, birth_year):
        self._name = name
        self._birth_year = birth_year

    @property
    def birth_year(self):
        """Read-only birth year."""
        return self._birth_year

    # No @birth_year.setter - read-only!

    @property
    def age(self):
        """Computed age based on birth year."""
        current_year = datetime.now().year
        return current_year - self._birth_year


class ImmutablePoint:
    """Point with read-only coordinates."""

    def __init__(self, x, y):
        self._x = x
        self._y = y

    @property
    def x(self):
        """Read-only x coordinate."""
        return self._x

    @property
    def y(self):
        """Read-only y coordinate."""
        return self._y

    @property
    def distance_from_origin(self):
        """Computed distance from origin."""
        return (self._x ** 2 + self._y ** 2) ** 0.5


class BankAccount:
    """Bank account with read-only account number."""

    def __init__(self, account_number, balance=0):
        self._account_number = account_number
        self._balance = balance
        self._transactions = []

    @property
    def account_number(self):
        """Read-only account number."""
        return self._account_number

    @property
    def balance(self):
        """Read-only balance (use deposit/withdraw)."""
        return self._balance

    @property
    def transactions(self):
        """Read-only copy of transactions."""
        return self._transactions.copy()  # Return copy, not original!

    def deposit(self, amount):
        """Deposit money."""
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount
        self._transactions.append(f"Deposit: +${amount}")

    def withdraw(self, amount):
        """Withdraw money."""
        if amount <= 0:
            raise ValueError("Withdrawal must be positive")
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount
        self._transactions.append(f"Withdrawal: -${amount}")


class Counter:
    """Counter with read-only count."""

    def __init__(self):
        self._count = 0

    @property
    def count(self):
        """Read-only count."""
        return self._count

    def increment(self):
        """Increment counter."""
        self._count += 1

    def reset(self):
        """Reset counter."""
        self._count = 0


def main():
    print("=== Read-only Properties ===\n")

    # Person with read-only birth year
    print("--- Person with Read-only Birth Year ---")
    person = Person("Alice", 1990)
    print(f"Name: {person._name}")
    print(f"Birth year: {person.birth_year}")
    print(f"Age: {person.age}")

    try:
        person.birth_year = 1995  # Attempt to modify
    except AttributeError as e:
        print(f"Error: {e}")

    # Immutable point
    print("\n--- Immutable Point ---")
    point = ImmutablePoint(3, 4)
    print(f"Point: ({point.x}, {point.y})")
    print(f"Distance from origin: {point.distance_from_origin}")

    try:
        point.x = 10
    except AttributeError as e:
        print(f"Error: {e}")

    # Bank account
    print("\n--- Bank Account ---")
    account = BankAccount("12345", 1000)
    print(f"Account: {account.account_number}")
    print(f"Balance: ${account.balance}")

    account.deposit(500)
    account.withdraw(200)
    print(f"New balance: ${account.balance}")

    print("Transactions:")
    for transaction in account.transactions:
        print(f"  - {transaction}")

    try:
        account.balance = 999999  # Attempt to cheat
    except AttributeError as e:
        print(f"Error: {e}")

    # Counter
    print("\n--- Counter ---")
    counter = Counter()
    print(f"Initial count: {counter.count}")

    counter.increment()
    counter.increment()
    counter.increment()
    print(f"After increments: {counter.count}")

    try:
        counter.count = 100
    except AttributeError as e:
        print(f"Error: {e}")

    counter.reset()
    print(f"After reset: {counter.count}")

    print("\n=== Key Points ===")
    print("""
1. Property without setter is read-only
2. Attempting to set raises AttributeError
3. Useful for IDs, computed values, immutable data
4. Provide methods (deposit/withdraw) instead of direct access
5. Return copies of mutable internal data
    """)


if __name__ == "__main__":
    main()


# import_datetime
# from datetime import datetime
#
# For calculating current year.
#

# init_person
# def __init__(self, name, birth_year):
#
# Store name and birth year.
#

# store_name
# self._name = name
#
# Store name.
#

# store_birth_year
# self._birth_year = birth_year
#
# Store birth year (immutable).
#

# birth_year_property
# @property
#
# Birth year getter only.
#

# birth_year_getter
# def birth_year(self):
#
# Get birth year.
#

# return_birth_year
# return self._birth_year
#
# Return birth year.
#

# no_setter
# No @birth_year.setter - read-only!
#
# No setter = read-only property.
#

# age_property
# @property
#
# Computed age.
#

# age_getter
# def age(self):
#
# Calculate current age.
#

# get_current_year
# current_year = datetime.now().year
#
# Get current year.
#

# calc_age
# return current_year - self._birth_year
#
# Calculate age.
#

# init_point
# def __init__(self, x, y):
#
# Store coordinates.
#

# store_x
# self._x = x
#
# Store x.
#

# store_y
# self._y = y
#
# Store y.
#

# x_property
# @property
#
# Read-only x.
#

# x_getter
# def x(self):
#
# Get x.
#

# return_x
# return self._x
#
# Return x.
#

# y_property
# @property
#
# Read-only y.
#

# y_getter
# def y(self):
#
# Get y.
#

# return_y
# return self._y
#
# Return y.
#

# distance_property
# @property
#
# Computed distance.
#

# distance_getter
# def distance_from_origin(self):
#
# Calculate distance from (0, 0).
#

# calc_distance
# return (self._x ** 2 + self._y ** 2) ** 0.5
#
# Pythagorean theorem.
#

# init_account
# def __init__(self, account_number, balance=0):
#
# Bank account constructor.
#

# store_account_number
# self._account_number = account_number
#
# Immutable account number.
#

# store_balance
# self._balance = balance
#
# Private balance.
#

# init_transactions
# self._transactions = []
#
# Transaction log.
#

# account_number_property
# @property
#
# Read-only account number.
#

# account_number_getter
# def account_number(self):
#
# Get account number.
#

# return_account_number
# return self._account_number
#
# Return account number.
#

# balance_property
# @property
#
# Read-only balance.
#

# balance_getter
# def balance(self):
#
# Get balance (no setter).
#

# return_balance
# return self._balance
#
# Return balance.
#

# transactions_property
# @property
#
# Read-only transactions.
#

# transactions_getter
# def transactions(self):
#
# Get transactions list.
#

# return_copy
# return self._transactions.copy()
#
# Return copy, not original!
# Prevents: account.transactions.append(...)
#

# deposit_method
# def deposit(self, amount):
#
# Method to modify balance.
#

# check_deposit_amount
# if amount <= 0:
#
# Validate positive.
#

# raise_deposit
# raise ValueError("Deposit must be positive")
#
# Reject non-positive.
#

# add_to_balance
# self._balance += amount
#
# Update balance.
#

# log_deposit
# self._transactions.append(f"Deposit: +${amount}")
#
# Log transaction.
#

# withdraw_method
# def withdraw(self, amount):
#
# Method to withdraw.
#

# check_withdraw_amount
# if amount <= 0:
#
# Validate positive.
#

# raise_withdraw_amount
# raise ValueError("Withdrawal must be positive")
#
# Reject non-positive.
#

# check_sufficient_funds
# if amount > self._balance:
#
# Check sufficient funds.
#

# raise_insufficient
# raise ValueError("Insufficient funds")
#
# Reject overdraft.
#

# subtract_from_balance
# self._balance -= amount
#
# Update balance.
#

# log_withdrawal
# self._transactions.append(f"Withdrawal: -${amount}")
#
# Log transaction.
#

# init_counter
# def __init__(self):
#
# Counter constructor.
#

# init_count
# self._count = 0
#
# Initialize to 0.
#

# count_property
# @property
#
# Read-only count.
#

# count_getter
# def count(self):
#
# Get count.
#

# return_count
# return self._count
#
# Return count.
#

# increment_method
# def increment(self):
#
# Method to increment.
#

# do_increment
# self._count += 1
#
# Increment count.
#

# reset_method
# def reset(self):
#
# Method to reset.
#

# do_reset
# self._count = 0
#
# Reset to 0.
#

# person_demo
# Person with read-only birth year
#
# Demonstrate read-only property.
#

# create_person
# person = Person("Alice", 1990)
#
# Create person.
#

# print_name
# print(f"Name: {person._name}")
#
# Access private attribute directly.
#

# print_birth_year
# print(f"Birth year: {person.birth_year}")
#
# Via property.
#

# print_age
# print(f"Age: {person.age}")
#
# Computed property.
#

# try_modify_birth_year
# try:
#
# Try to modify.
#

# attempt_modify
# person.birth_year = 1995
#
# Attempt to set read-only property.
#

# catch_attribute_error
# except AttributeError as e:
#
# Raised when no setter.
#

# print_readonly_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#

# point_demo
# Immutable point
#
# Read-only coordinates.
#

# create_point
# point = ImmutablePoint(3, 4)
#
# Create point at (3, 4).
#

# print_point
# print(f"Point: ({point.x}, {point.y})")
#
# "Point: (3, 4)"
#

# print_distance
# print(f"Distance from origin: {point.distance_from_origin}")
#
# "Distance from origin: 5.0"
#

# try_modify_point
# try:
#
# Try to modify.
#

# attempt_modify_x
# point.x = 10
#
# Attempt to set x.
#

# catch_point_error
# except AttributeError as e:
#
# No setter for x.
#

# print_point_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#

# account_demo
# Bank account
#
# Read-only balance and account number.
#

# create_account
# account = BankAccount("12345", 1000)
#
# Create account.
#

# print_account_number
# print(f"Account: {account.account_number}")
#
# "Account: 12345"
#

# print_balance
# print(f"Balance: ${account.balance}")
#
# "Balance: $1000"
#

# do_deposit
# account.deposit(500)
#
# Add $500.
#

# do_withdraw
# account.withdraw(200)
#
# Withdraw $200.
#

# print_new_balance
# print(f"New balance: ${account.balance}")
#
# "New balance: $1300"
#

# print_transactions_header
# print("Transactions:")
#
# Show transactions.
#

# loop_transactions
# for transaction in account.transactions:
#
# Loop through copy.
#

# print_transaction
# print(f"  - {transaction}")
#
# - Deposit: +$500
# - Withdrawal: -$200
#

# try_modify_balance
# try:
#
# Try to cheat.
#

# attempt_modify_balance
# account.balance = 999999
#
# Attempt to set balance directly.
#

# catch_balance_error
# except AttributeError as e:
#
# No setter for balance.
#

# print_balance_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#

# counter_demo
# Counter
#
# Read-only count.
#

# create_counter
# counter = Counter()
#
# Create counter.
#

# print_initial_count
# print(f"Initial count: {counter.count}")
#
# "Initial count: 0"
#

# inc1
# counter.increment()
#
# Count: 1
#

# inc2
# counter.increment()
#
# Count: 2
#

# inc3
# counter.increment()
#
# Count: 3
#

# print_after_increments
# print(f"After increments: {counter.count}")
#
# "After increments: 3"
#

# try_modify_count
# try:
#
# Try to modify.
#

# attempt_modify_count
# counter.count = 100
#
# Attempt to set count.
#

# catch_count_error
# except AttributeError as e:
#
# No setter for count.
#

# print_count_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#

# do_reset
# counter.reset()
#
# Reset to 0.
#

# print_after_reset
# print(f"After reset: {counter.count}")
#
# "After reset: 0"
#
  1. """Person with read-only birth_year."""

    6class Person:7    """Person with read-only birth_year."""89    def __init__(self, name, birth_year): #?init_person10        self._name = name #?store_name11        self._birth_year = birth_year #?store_birth_year1213    @property #?birth_year_property14    def birth_year(self): #?birth_year_getter15        """Read-only birth year."""16        return self._birth_year #?return_birth_year1718    # No @birth_year.setter - read-only! #?no_setter1920    @property #?age_property21    def age(self): #?age_getter22        """Computed age based on birth year."""23        current_year = datetime.now().year #?get_current_year24        return current_year - self._birth_year #?calc_age252627class ImmutablePoint:28    """Point with read-only coordinates."""2930    def __init__(self, x, y): #?init_point31        self._x = x #?store_x32        self._y = y #?store_y3334    @property #?x_property35    def x(self): #?x_getter36        """Read-only x coordinate."""37        return self._x #?return_x3839    @property #?y_property40    def y(self): #?y_getter41        """Read-only y coordinate."""42        return self._y #?return_y4344    @property #?distance_property45    def distance_from_origin(self): #?distance_getter46        """Computed distance from origin."""47        return (self._x ** 2 + self._y ** 2) ** 0.5 #?calc_distance484950class BankAccount:51    """Bank account with read-only account number."""5253    def __init__(self, account_number, balance=0): #?init_account54        self._account_number = account_number #?store_account_number55        self._balance = balance #?store_balance56        self._transactions = [] #?init_transactions5758    @property #?account_number_property59    def account_number(self): #?account_number_getter60        """Read-only account number."""61        return self._account_number #?return_account_number6263    @property #?balance_property64    def balance(self): #?balance_getter65        """Read-only balance (use deposit/withdraw)."""66        return self._balance #?return_balance6768    @property #?transactions_property69    def transactions(self): #?transactions_getter70        """Read-only copy of transactions."""71        return self._transactions.copy()  # Return copy, not original!7273    def deposit(self, amount): #?deposit_method74        """Deposit money."""75        if amount <= 0: #?check_deposit_amount76            raise ValueError("Deposit must be positive") #?raise_deposit77        self._balance += amount #?add_to_balance78        self._transactions.append(f"Deposit: +${amount}") #?log_deposit7980    def withdraw(self, amount): #?withdraw_method81        """Withdraw money."""82        if amount <= 0: #?check_withdraw_amount83            raise ValueError("Withdrawal must be positive") #?raise_withdraw_amount84        if amount > self._balance: #?check_sufficient_funds85            raise ValueError("Insufficient funds") #?raise_insufficient86        self._balance -= amount #?subtract_from_balance87        self._transactions.append(f"Withdrawal: -${amount}") #?log_withdrawal888990class Counter:91    """Counter with read-only count."""
  2. def main():

    110def main():111    print("=== Read-only Properties ===\n")112113    # Person with read-only birth year #?person_demo114    print("--- Person with Read-only Birth Year ---")115    person = Person("Alice", 1990) #?create_person116    print(f"Name: {person._name}") #?print_name
    output=== Read-only Properties ===
    --- Person with Read-only Birth Year ---
  3. self._name ← Alice, self._birth_year ← 1990

    9def __init__(self⟨Person A⟩, nameAlice, birth_year1990): #?init_person10    self._name→ Alice = nameAlice #?store_name11    self._birth_year→ 1990 = birth_year1990 #?store_birth_year
  4. person ← ⟨Person A⟩

    114print("--- Person with Read-only Birth Year ---")115person→ ⟨Person A⟩ = Person("Alice", 1990) #?create_person116print(f"Name: {person._nameAlice}") #?print_name117print(f"Birth year: {person.birth_year1990}") #?print_birth_year118print(f"Age: {person.age}") #?print_age
    outputName: Alice
  5. def birth_year(self): #?birth_year_getter

    13@property #?birth_year_property14def birth_year(self⟨Person A⟩): #?birth_year_getter15    """Read-only birth year."""16    return self._birth_year1990 #?return_birth_year
  6. print(f"Birth year: {person.birth_year}") #?print_birth_year

    116print(f"Name: {person._name}") #?print_name117print(f"Birth year: {person.birth_year1990}") #?print_birth_year118print(f"Age: {person.age36}") #?print_age
    outputBirth year: 1990
  7. current_year ← 2026

    20@property #?age_property21def age(self⟨Person A⟩): #?age_getter22    """Computed age based on birth year."""23    current_year→ 2026 = datetime<class 'datetime.datetime'>.now().year #?get_current_year24    return current_year2026 - self._birth_year1990 #?calc_age
  8. print(f"Age: {person.age}") #?print_age

    117print(f"Birth year: {person.birth_year}") #?print_birth_year118print(f"Age: {person.age36}") #?print_age
    outputAge: 36
  9. except AttributeError as e: #?catch_attribute_error

    121    person.birth_year = 1995  # Attempt to modify122except AttributeError as e: #?catch_attribute_error123    print(f"Error: {eproperty 'birth_year' of 'Person' object has no setter}") #?print_readonly_error124125# Immutable point #?point_demo
    outputError: property 'birth_year' of 'Person' object has no setter
    Error: property 'birth_year' of 'Person' object has no setter
  10. print(" --- Immutable Point ---")

    125# Immutable point #?point_demo126print("\n--- Immutable Point ---")127point = ImmutablePoint(3, 4) #?create_point128print(f"Point: ({point.x}, {point.y})") #?print_point
    output
    --- Immutable Point ---
  11. self._x ← 3, self._y ← 4

    30def __init__(self⟨ImmutablePoint B⟩, x3, y4): #?init_point31    self._x→ 3 = x3 #?store_x32    self._y→ 4 = y4 #?store_y
  12. point ← ⟨ImmutablePoint B⟩

    126print("\n--- Immutable Point ---")127point→ ⟨ImmutablePoint B⟩ = ImmutablePoint(3, 4) #?create_point128print(f"Point: ({point.x3}, {point.y4})") #?print_point129print(f"Distance from origin: {point.distance_from_origin}") #?print_distance
  13. def x(self): #?x_getter

    34@property #?x_property35def x(self⟨ImmutablePoint B⟩): #?x_getter36    """Read-only x coordinate."""37    return self._x3 #?return_x
  14. def y(self): #?y_getter

    39@property #?y_property40def y(self⟨ImmutablePoint B⟩): #?y_getter41    """Read-only y coordinate."""42    return self._y4 #?return_y
  15. print(f"Point: ({point.x}, {point.y})") #?print_point

    127point = ImmutablePoint(3, 4) #?create_point128print(f"Point: ({point.x3}, {point.y4})") #?print_point129print(f"Distance from origin: {point.distance_from_origin5.0}") #?print_distance
    outputPoint: (3, 4)
  16. def distance_from_origin(self): #?distance_getter

    44@property #?distance_property45def distance_from_origin(self⟨ImmutablePoint B⟩): #?distance_getter46    """Computed distance from origin."""47    return (self._x3 ** 2 + self._y4 ** 2) ** 0.5 #?calc_distance
  17. print(f"Distance from origin: {point.distance_from_origin}") #?print_d…

    128print(f"Point: ({point.x}, {point.y})") #?print_point129print(f"Distance from origin: {point.distance_from_origin5.0}") #?print_distance
    outputDistance from origin: 5.0
  18. except AttributeError as e: #?catch_point_error

    132    point.x = 10 #?attempt_modify_x133except AttributeError as e: #?catch_point_error134    print(f"Error: {eproperty 'x' of 'ImmutablePoint' object has no setter}") #?print_point_error135136# Bank account #?account_demo
    outputError: property 'x' of 'ImmutablePoint' object has no setter
    Error: property 'x' of 'ImmutablePoint' object has no setter
  19. print(" --- Bank Account ---")

    136# Bank account #?account_demo137print("\n--- Bank Account ---")138account = BankAccount("12345", 1000) #?create_account139print(f"Account: {account.account_number}") #?print_account_number
    output
    --- Bank Account ---
  20. self._account_number ← 12345, self._balance ← 1000, self._transactions ← []

    53def __init__(self⟨BankAccount C⟩, account_number12345, balance1000=0): #?init_account54    self._account_number→ 12345 = account_number12345 #?store_account_number55    self._balance→ 1000 = balance1000 #?store_balance56    self._transactions→ [] = [] #?init_transactions
  21. account ← ⟨BankAccount C⟩

    137print("\n--- Bank Account ---")138account→ ⟨BankAccount C⟩ = BankAccount("12345", 1000) #?create_account139print(f"Account: {account.account_number12345}") #?print_account_number140print(f"Balance: ${account.balance}") #?print_balance
  22. def account_number(self): #?account_number_getter

    58@property #?account_number_property59def account_number(self⟨BankAccount C⟩): #?account_number_getter60    """Read-only account number."""61    return self._account_number12345 #?return_account_number
  23. print(f"Account: {account.account_number}") #?print_account_number

    138account = BankAccount("12345", 1000) #?create_account139print(f"Account: {account.account_number12345}") #?print_account_number140print(f"Balance: ${account.balance1000}") #?print_balance
    outputAccount: 12345
  24. def balance(self): #?balance_getter

    pass 1 of 2
    63@property #?balance_property64def balance(self⟨BankAccount C⟩): #?balance_getter65    """Read-only balance (use deposit/withdraw)."""66    return self._balance1000 #?return_balance
  25. print(f"Balance: ${account.balance}") #?print_balance

    139print(f"Account: {account.account_number}") #?print_account_number140print(f"Balance: ${account.balance1000}") #?print_balance141142account⟨BankAccount C⟩.deposit(500) #?do_deposit143account.withdraw(200) #?do_withdraw
    outputBalance: $1000
  26. self._balance ← 1500, self._transactions ← ['Deposit: +$500']

    73def deposit(self⟨BankAccount C⟩, amount500): #?deposit_method74    """Deposit money."""75    if amount <= 0: #?check_deposit_amount76        raise ValueError("Deposit must be positive") #?raise_deposit77    self._balance→ 1500 += amount500 #?add_to_balance78    self._transactions→ ['Deposit: +$500'].append(f"Deposit: +${amount500}") #?log_deposit
  27. account.deposit(500) #?do_deposit

    142account⟨BankAccount C⟩.deposit(500) #?do_deposit143account⟨BankAccount C⟩.withdraw(200) #?do_withdraw144print(f"New balance: ${account.balance}") #?print_new_balance
  28. self._balance ← 1300, self._transactions ← ['Deposit: +$500', 'Withdrawal: -$200']

    80def withdraw(self⟨BankAccount C⟩, amount200): #?withdraw_method81    """Withdraw money."""82    if amount <= 0: #?check_withdraw_amount83        raise ValueError("Withdrawal must be positive") #?raise_withdraw_amount84    if amount > self._balance: #?check_sufficient_funds85        raise ValueError("Insufficient funds") #?raise_insufficient86    self._balance→ 1300 -= amount200 #?subtract_from_balance87    self._transactions→ ['Deposit: +$500', 'Withdrawal: -$200'].append(f"Withdrawal: -${amount200}") #?log_withdrawal
  29. account.withdraw(200) #?do_withdraw

    142account.deposit(500) #?do_deposit143account⟨BankAccount C⟩.withdraw(200) #?do_withdraw144print(f"New balance: ${account.balance1300}") #?print_new_balance
  30. def balance(self): #?balance_getter

    pass 2 of 2
    63@property #?balance_property64def balance(self⟨BankAccount C⟩): #?balance_getter65    """Read-only balance (use deposit/withdraw)."""66    return self._balance1300 #?return_balance
  31. print(f"New balance: ${account.balance}") #?print_new_balance

    143account.withdraw(200) #?do_withdraw144print(f"New balance: ${account.balance1300}") #?print_new_balance145146print("Transactions:") #?print_transactions_header147for transaction in account.transactions: #?loop_transactions
    outputNew balance: $1300
    Transactions:
  32. def transactions(self): #?transactions_getter

    68@property #?transactions_property69def transactions(self⟨BankAccount C⟩): #?transactions_getter70    """Read-only copy of transactions."""71    return self._transactions['Deposit: +$500', 'Withdrawal: -$200'].copy()  # Return copy, not original!
  33. for transaction in account.transactions: #?loop_transactions

    pass 1 of 2
    146print("Transactions:") #?print_transactions_header147for transactionDeposit: +$500 in account.transactions['Deposit: +$500', 'Withdrawal: -$200']: #?loop_transactions148    print(f"  - {transactionDeposit: +$500}") #?print_transaction
    output  - Deposit: +$500
  34. for transaction in account.transactions: #?loop_transactions

    pass 2 of 2
    146print("Transactions:") #?print_transactions_header147for transactionWithdrawal: -$200 in account.transactions['Deposit: +$500', 'Withdrawal: -$200']: #?loop_transactions148    print(f"  - {transactionWithdrawal: -$200}") #?print_transaction
    output  - Withdrawal: -$200
  35. except AttributeError as e: #?catch_balance_error

    151    account.balance = 999999  # Attempt to cheat152except AttributeError as e: #?catch_balance_error153    print(f"Error: {eproperty 'balance' of 'BankAccount' object has no setter}") #?print_balance_error154155# Counter #?counter_demo
    outputError: property 'balance' of 'BankAccount' object has no setter
    Error: property 'balance' of 'BankAccount' object has no setter
  36. print(" --- Counter ---")

    155# Counter #?counter_demo156print("\n--- Counter ---")157counter = Counter() #?create_counter158print(f"Initial count: {counter.count}") #?print_initial_count
    output
    --- Counter ---
  37. self._count ← 0

    93def __init__(self⟨Counter D⟩): #?init_counter94    self._count→ 0 = 0 #?init_count
  38. counter ← ⟨Counter D⟩

    156print("\n--- Counter ---")157counter→ ⟨Counter D⟩ = Counter() #?create_counter158print(f"Initial count: {counter.count0}") #?print_initial_count
  39. def count(self): #?count_getter

    pass 1 of 3
    96@property #?count_property97def count(self⟨Counter D⟩): #?count_getter98    """Read-only count."""99    return self._count0 #?return_count
    All 3 passes — pass 1 is the card above
    passself._count
    10
    23
    30
  40. print(f"Initial count: {counter.count}") #?print_initial_count

    157counter = Counter() #?create_counter158print(f"Initial count: {counter.count0}") #?print_initial_count159160counter⟨Counter D⟩.increment() #?inc1161counter.increment() #?inc2
    outputInitial count: 0
  41. self._count ← 1

    pass 1 of 3
    101def increment(self⟨Counter D⟩): #?increment_method102    """Increment counter."""103    self._count→ 1 += 1 #?do_increment
    All 3 passes — pass 1 is the card above
    passself._count
    10 1
    21 2
    32 3
  42. counter.increment() #?inc1

    160counter⟨Counter D⟩.increment() #?inc1161counter⟨Counter D⟩.increment() #?inc2162counter.increment() #?inc3
  43. counter.increment() #?inc2

    160counter.increment() #?inc1161counter⟨Counter D⟩.increment() #?inc2162counter⟨Counter D⟩.increment() #?inc3163print(f"After increments: {counter.count}") #?print_after_increments
  44. counter.increment() #?inc3

    161counter.increment() #?inc2162counter⟨Counter D⟩.increment() #?inc3163print(f"After increments: {counter.count3}") #?print_after_increments
  45. print(f"After increments: {counter.count}") #?print_after_increments

    162counter.increment() #?inc3163print(f"After increments: {counter.count3}") #?print_after_increments
    outputAfter increments: 3
  46. except AttributeError as e: #?catch_count_error

    166    counter.count = 100 #?attempt_modify_count167except AttributeError as e: #?catch_count_error168    print(f"Error: {eproperty 'count' of 'Counter' object has no setter}") #?print_count_error169170counter.reset() #?do_reset
    outputError: property 'count' of 'Counter' object has no setter
    Error: property 'count' of 'Counter' object has no setter
  47. counter.reset() #?do_reset

    170counter⟨Counter D⟩.reset() #?do_reset171print(f"After reset: {counter.count}") #?print_after_reset
  48. self._count ← 0

    105def reset(self⟨Counter D⟩): #?reset_method106    """Reset counter."""107    self._count→ 0 = 0 #?do_reset
  49. counter.reset() #?do_reset

    170counter⟨Counter D⟩.reset() #?do_reset171print(f"After reset: {counter.count0}") #?print_after_reset
  50. print(f"After reset: {counter.count}") #?print_after_reset

    170    counter.reset() #?do_reset171    print(f"After reset: {counter.count0}") #?print_after_reset172173    print("\n=== Key Points ===")174    print("""1751. Property without setter is read-only1762. Attempting to set raises AttributeError1773. Useful for IDs, computed values, immutable data1784. Provide methods (deposit/withdraw) instead of direct access1795. Return copies of mutable internal data180    """)
    outputAfter reset: 0
    
    === Key Points ===
    
    1. Property without setter is read-only
    2. Attempting to set raises AttributeError
    3. Useful for IDs, computed values, immutable data
    4. Provide methods (deposit/withdraw) instead of direct access
    5. Return copies of mutable internal data
        
  51. main()

    183if __name__ == "__main__":184    main()

Provide getter without setter. Assignment raises AttributeError.

Migrating to properties

Change implementation without breaking API.

migration.py
Replay: real traced execution (multi-file project)
# Migrating from Direct Attributes to Properties

# Version 1: Direct attribute access (old way)
class UserV1:
    """Original version with direct attribute access."""

    def __init__(self, username, email):
        self.username = username  # Direct attribute
        self.email = email  # Direct attribute


# Version 2: Using properties (new way - backward compatible!)
class UserV2:
    """Refactored with properties - same interface!"""

    def __init__(self, username, email):
        self.username = username  # Uses setter
        self.email = email  # Uses setter

    @property
    def username(self):
        """Get username."""
        return self._username

    @username.setter
    def username(self, value):
        """Set username with validation (NEW!)."""
        if not isinstance(value, str) or len(value) < 3:
            raise ValueError("Username must be string with 3+ chars")
        self._username = value

    @property
    def email(self):
        """Get email."""
        return self._email

    @email.setter
    def email(self, value):
        """Set email with validation (NEW!)."""
        if "@" not in value:
            raise ValueError("Invalid email format")
        self._email = value.lower()  # Normalize to lowercase (NEW!)


# Old API - direct access
class LegacyCart:
    """Old shopping cart with direct access."""

    def __init__(self):
        self.items = []
        self.total = 0

    def add_item(self, price):
        """Add item (user must update total manually - error-prone!)."""
        self.items.append(price)
        # User responsible for updating total!


# New API - using properties
class ModernCart:
    """Modernized cart with computed total."""

    def __init__(self):
        self._items = []

    @property
    def items(self):
        """Get items (read-only copy)."""
        return self._items.copy()

    @property
    def total(self):
        """Computed total (always accurate!)."""
        return sum(self._items)

    def add_item(self, price):
        """Add item (total auto-updates!)."""
        if price < 0:
            raise ValueError("Price cannot be negative")
        self._items.append(price)
        # No manual total update needed!


def main():
    print("=== Migrating to Properties ===\n")

    # Version 1: Direct attribute access
    print("--- Version 1: Direct Attributes ---")
    user_v1 = UserV1("alice", "ALICE@EXAMPLE.COM")
    print(f"Username: {user_v1.username}")
    print(f"Email: {user_v1.email}")

    # Problems with V1:
    user_v1.username = "ab"  # No validation - BAD DATA!
    user_v1.email = "not-an-email"  # No validation - BAD DATA!
    print(f"Invalid username accepted: {user_v1.username}")
    print(f"Invalid email accepted: {user_v1.email}")

    # Version 2: Properties (backward compatible interface!)
    print("\n--- Version 2: Properties ---")
    user_v2 = UserV2("alice", "ALICE@EXAMPLE.COM")
    print(f"Username: {user_v2.username}")  # Same interface!
    print(f"Email: {user_v2.email}")  # But normalized!

    # Now with validation:
    try:
        user_v2.username = "ab"  # Now validated!
    except ValueError as e:
        print(f"Validation error: {e}")

    # Legacy cart problems
    print("\n--- Legacy Cart (Manual Total) ---")
    legacy = LegacyCart()
    legacy.add_item(10.00)
    legacy.add_item(20.00)
    print(f"Items: {legacy.items}")
    print(f"Total: ${legacy.total}")  # Still 0 - forgot to update!

    # Forgot to update total!
    legacy.total = 30.00  # Manual and error-prone
    print(f"After manual update: ${legacy.total}")

    # Modern cart with computed property
    print("\n--- Modern Cart (Computed Total) ---")
    modern = ModernCart()
    modern.add_item(10.00)
    modern.add_item(20.00)
    print(f"Items: {modern.items}")
    print(f"Total: ${modern.total}")  # Always correct!

    modern.add_item(15.00)
    print(f"After adding item: ${modern.total}")  # Auto-updated!

    # Can't accidentally set wrong total
    try:
        modern.total = 9999
    except AttributeError as e:
        print(f"Error: {e}")

    print("\n=== Migration Benefits ===")
    print("""
1. Backward compatible - same interface
2. Add validation without breaking existing code
3. Transform data (e.g., normalize email)
4. Computed values always accurate
5. Prevent incorrect manual updates
6. Read-only computed properties
    """)


if __name__ == "__main__":
    main()


# version1
# Version 1: Direct attribute access (old way)
#
# Original implementation.
#

# init_v1
# def __init__(self, username, email):
#
# V1 constructor.
#

# direct_username
# self.username = username
#
# Public attribute (no validation).
#

# direct_email
# self.email = email
#
# Public attribute (no normalization).
#

# version2
# Version 2: Using properties (new way - backward compatible!)
#
# Refactored with properties.
# Same interface as V1!
#

# init_v2
# def __init__(self, username, email):
#
# V2 constructor.
#

# prop_username_init
# self.username = username
#
# Triggers username setter (validates!).
#

# prop_email_init
# self.email = email
#
# Triggers email setter (validates & normalizes!).
#

# username_property_v2
# @property
#
# Username property.
#

# username_getter_v2
# def username(self):
#
# Get username.
#

# return_username_v2
# return self._username
#
# Return stored value.
#

# username_setter_v2
# @username.setter
#
# Username setter with validation.
#

# username_setter_method_v2
# def username(self, value):
#
# Set username.
#

# validate_username_v2
# if not isinstance(value, str) or len(value) < 3:
#
# NEW: Validate input.
#

# raise_username_v2
# raise ValueError("Username must be string with 3+ chars")
#
# NEW: Reject invalid.
#

# store_username_v2
# self._username = value
#
# Store valid value.
#

# email_property_v2
# @property
#
# Email property.
#

# email_getter_v2
# def email(self):
#
# Get email.
#

# return_email_v2
# return self._email
#
# Return stored email.
#

# email_setter_v2
# @email.setter
#
# Email setter.
#

# email_setter_method_v2
# def email(self, value):
#
# Set email.
#

# validate_email_v2
# if "@" not in value:
#
# NEW: Validate format.
#

# raise_email_v2
# raise ValueError("Invalid email format")
#
# NEW: Reject invalid.
#

# store_email_v2
# self._email = value.lower()
#
# NEW: Normalize to lowercase!
#

# old_api
# Old API - direct access
#
# Legacy cart with manual total.
#

# init_legacy_cart
# def __init__(self):
#
# Legacy constructor.
#

# direct_items
# self.items = []
#
# Public items list.
#

# direct_total
# self.total = 0
#
# Public total (manual).
#

# add_item_legacy
# def add_item(self, price):
#
# Add item method.
#

# append_item
# self.items.append(price)
#
# Add to items.
#

# manual_total
# User responsible for updating total!
#
# Error-prone!
#

# new_api
# New API - using properties
#
# Modern cart with computed total.
#

# init_modern_cart
# def __init__(self):
#
# Modern constructor.
#

# private_items
# self._items = []
#
# Private items list.
#

# items_property
# @property
#
# Items property (read-only).
#

# items_getter
# def items(self):
#
# Get items.
#

# return_items_copy
# return self._items.copy()
#
# Return copy (prevent modification).
#

# total_property
# @property
#
# Computed total.
#

# total_getter
# def total(self):
#
# Calculate total.
#

# calc_total
# return sum(self._items)
#
# Always accurate!
#

# add_item_modern
# def add_item(self, price):
#
# Add item (modern).
#

# validate_price
# if price < 0:
#
# Validate price.
#

# raise_price
# raise ValueError("Price cannot be negative")
#
# Reject negative.
#

# append_modern_item
# self._items.append(price)
#
# Add to items.
#

# auto_total
# No manual total update needed!
#
# Total auto-computed.
#

# demo_v1
# Version 1: Direct attribute access
#
# Show V1 problems.
#

# create_v1
# user_v1 = UserV1("alice", "ALICE@EXAMPLE.COM")
#
# Create V1 user.
#

# print_v1_username
# print(f"Username: {user_v1.username}")
#
# "Username: alice"
#

# print_v1_email
# print(f"Email: {user_v1.email}")
#
# "Email: ALICE@EXAMPLE.COM" (not normalized)
#

# v1_problems
# Problems with V1:
#
# No validation or normalization.
#

# set_invalid_v1
# user_v1.username = "ab"
#
# Too short but accepted!
#

# set_invalid_email_v1
# user_v1.email = "not-an-email"
#
# Invalid but accepted!
#

# print_invalid_v1
# print(f"Invalid username accepted: {user_v1.username}")
#
# "Invalid username accepted: ab"
#

# print_invalid_email_v1
# print(f"Invalid email accepted: {user_v1.email}")
#
# "Invalid email accepted: not-an-email"
#

# demo_v2
# Version 2: Properties (backward compatible interface!)
#
# Show V2 improvements.
#

# create_v2
# user_v2 = UserV2("alice", "ALICE@EXAMPLE.COM")
#
# Create V2 user.
#

# print_v2_username
# print(f"Username: {user_v2.username}")
#
# Same interface as V1!
# "Username: alice"
#

# print_v2_email
# print(f"Email: {user_v2.email}")
#
# "Email: alice@example.com" (normalized!)
#

# v2_validation
# Now with validation:
#
# Properties add validation.
#

# try_invalid_v2
# try:
#
# Try invalid data.
#

# set_invalid_v2
# user_v2.username = "ab"
#
# Now rejected!
#

# catch_v2_error
# except ValueError as e:
#
# Validation error.
#

# print_v2_error
# print(f"Validation error: {e}")
#
# "Validation error: Username must be string with 3+ chars"
#

# legacy_cart_demo
# Legacy cart problems
#
# Manual total is error-prone.
#

# create_legacy
# legacy = LegacyCart()
#
# Create legacy cart.
#

# add_legacy_1
# legacy.add_item(10.00)
#
# Add $10 item.
#

# add_legacy_2
# legacy.add_item(20.00)
#
# Add $20 item.
#

# print_legacy_items
# print(f"Items: {legacy.items}")
#
# "Items: [10.0, 20.0]"
#

# print_legacy_total
# print(f"Total: ${legacy.total}")
#
# "Total: $0" - WRONG! Forgot to update.
#

# forgot_total
# Forgot to update total!
#
# Common bug with manual total.
#

# manual_total_update
# legacy.total = 30.00
#
# Manual update (error-prone).
#

# print_manual_total
# print(f"After manual update: ${legacy.total}")
#
# "After manual update: $30.0"
#

# modern_cart_demo
# Modern cart with computed property
#
# Always accurate.
#

# create_modern
# modern = ModernCart()
#
# Create modern cart.
#

# add_modern_1
# modern.add_item(10.00)
#
# Add $10.
#

# add_modern_2
# modern.add_item(20.00)
#
# Add $20.
#

# print_modern_items
# print(f"Items: {modern.items}")
#
# "Items: [10.0, 20.0]"
#

# print_modern_total
# print(f"Total: ${modern.total}")
#
# "Total: $30.0" - Always correct!
#

# add_modern_3
# modern.add_item(15.00)
#
# Add $15.
#

# print_updated_total
# print(f"After adding item: ${modern.total}")
#
# "After adding item: $45.0" - Auto-updated!
#

# cant_set_total
# Can't accidentally set wrong total
#
# Read-only property.
#

# try_set_total
# try:
#
# Try to cheat.
#

# attempt_set_total
# modern.total = 9999
#
# No setter!
#

# catch_total_error
# except AttributeError as e:
#
# Can't set.
#

# print_total_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#
  1. """Original version with direct attribute access."""

    4class UserV1:5    """Original version with direct attribute access."""67    def __init__(self, username, email): #?init_v18        self.username = username  # Direct attribute9        self.email = email  # Direct attribute101112# Version 2: Using properties (new way - backward compatible!) #?version213class UserV2:14    """Refactored with properties - same interface!"""1516    def __init__(self, username, email): #?init_v217        self.username = username  # Uses setter18        self.email = email  # Uses setter1920    @property #?username_property_v221    def username(self): #?username_getter_v222        """Get username."""23        return self._username #?return_username_v22425    @username.setter #?username_setter_v226    def username(self, value): #?username_setter_method_v227        """Set username with validation (NEW!)."""28        if not isinstance(value, str) or len(value) < 3: #?validate_username_v229            raise ValueError("Username must be string with 3+ chars") #?raise_username_v230        self._username = value #?store_username_v23132    @property #?email_property_v233    def email(self): #?email_getter_v234        """Get email."""35        return self._email #?return_email_v23637    @email.setter #?email_setter_v238    def email(self, value): #?email_setter_method_v239        """Set email with validation (NEW!)."""40        if "@" not in value: #?validate_email_v241            raise ValueError("Invalid email format") #?raise_email_v242        self._email = value.lower()  # Normalize to lowercase (NEW!)434445# Old API - direct access #?old_api46class LegacyCart:47    """Old shopping cart with direct access."""4849    def __init__(self): #?init_legacy_cart50        self.items = [] #?direct_items51        self.total = 0 #?direct_total5253    def add_item(self, price): #?add_item_legacy54        """Add item (user must update total manually - error-prone!)."""55        self.items.append(price) #?append_item56        # User responsible for updating total! #?manual_total575859# New API - using properties #?new_api60class ModernCart:61    """Modernized cart with computed total."""
  2. def main():

    84def main():85    print("=== Migrating to Properties ===\n")8687    # Version 1: Direct attribute access #?demo_v188    print("--- Version 1: Direct Attributes ---")89    user_v1 = UserV1("alice", "ALICE@EXAMPLE.COM") #?create_v190    print(f"Username: {user_v1.username}") #?print_v1_username
    output=== Migrating to Properties ===
    --- Version 1: Direct Attributes ---
  3. self.username ← alice, self.email ← ALICE@EXAMPLE.COM

    7def __init__(self⟨UserV1 A⟩, usernamealice, emailALICE@EXAMPLE.COM): #?init_v18    self.username→ alice = usernamealice  # Direct attribute9    self.email→ ALICE@EXAMPLE.COM = emailALICE@EXAMPLE.COM  # Direct attribute
  4. user_v1 ← ⟨UserV1 A⟩, user_v1.username ← ab, user_v1.email ← not-an-email

    88print("--- Version 1: Direct Attributes ---")89user_v1→ ⟨UserV1 A⟩ = UserV1("alice", "ALICE@EXAMPLE.COM") #?create_v190print(f"Username: {user_v1.usernamealice}") #?print_v1_username91print(f"Email: {user_v1.emailALICE@EXAMPLE.COM}") #?print_v1_email9293# Problems with V1: #?v1_problems94user_v1.username→ ab = "ab"  # No validation - BAD DATA!95user_v1.email→ not-an-email = "not-an-email"  # No validation - BAD DATA!96print(f"Invalid username accepted: {user_v1.usernameab}") #?print_invalid_v197print(f"Invalid email accepted: {user_v1.emailnot-an-email}") #?print_invalid_email_v19899# Version 2: Properties (backward compatible interface!) #?demo_v2100print("\n--- Version 2: Properties ---")101user_v2 = UserV2("alice", "ALICE@EXAMPLE.COM") #?create_v2102print(f"Username: {user_v2.username}")  # Same interface!
    outputUsername: alice
    Email: ALICE@EXAMPLE.COM
    Invalid username accepted: ab
    Invalid email accepted: not-an-email
    
    --- Version 2: Properties ---
  5. def __init__(self, username, email): #?init_v2

    16def __init__(self⟨UserV2 B⟩, usernamealice, emailALICE@EXAMPLE.COM): #?init_v217    self.username = usernamealice  # Uses setter18    self.email = email  # Uses setter
  6. self._username ← alice, self.username ← alice

    pass 1 of 2
    16def __init__(self, username, email): #?init_v217    self.username→ alice = usernamealice  # Uses setter18    self.email = emailALICE@EXAMPLE.COM  # Uses setter1920@property #?username_property_v221def username(self): #?username_getter_v222    """Get username."""23    return self._username #?return_username_v22425@username.setter #?username_setter_v226def username(self⟨UserV2 B⟩, valuealice): #?username_setter_method_v227    """Set username with validation (NEW!)."""28    if not isinstance(value, str) or len(value) < 3: #?validate_username_v229        raise ValueError("Username must be string with 3+ chars") #?raise_username_v230    self._username→ alice = valuealice #?store_username_v2
  7. self._email ← alice@example.com, self.email ← alice@example.com

    17    self.username = username  # Uses setter18    self.email→ alice@example.com = emailALICE@EXAMPLE.COM  # Uses setter1920@property #?username_property_v221def username(self): #?username_getter_v222    """Get username."""23    return self._username #?return_username_v22425@username.setter #?username_setter_v226def username(self, value): #?username_setter_method_v227    """Set username with validation (NEW!)."""28    if not isinstance(value, str) or len(value) < 3: #?validate_username_v229        raise ValueError("Username must be string with 3+ chars") #?raise_username_v230    self._username = value #?store_username_v23132@property #?email_property_v233def email(self): #?email_getter_v234    """Get email."""35    return self._email #?return_email_v23637@email.setter #?email_setter_v238def email(self⟨UserV2 B⟩, valueALICE@EXAMPLE.COM): #?email_setter_method_v239    """Set email with validation (NEW!)."""40    if "@" not in value: #?validate_email_v241        raise ValueError("Invalid email format") #?raise_email_v242    self._email→ alice@example.com = valueALICE@EXAMPLE.COM.lower()  # Normalize to lowercase (NEW!)
  8. user_v2 ← ⟨UserV2 B⟩

    100print("\n--- Version 2: Properties ---")101user_v2→ ⟨UserV2 B⟩ = UserV2("alice", "ALICE@EXAMPLE.COM") #?create_v2102print(f"Username: {user_v2.usernamealice}")  # Same interface!103print(f"Email: {user_v2.email}")  # But normalized!
  9. def username(self): #?username_getter_v2

    20@property #?username_property_v221def username(self⟨UserV2 B⟩): #?username_getter_v222    """Get username."""23    return self._usernamealice #?return_username_v2
  10. print(f"Username: {user_v2.username}") # Same interface!

    101user_v2 = UserV2("alice", "ALICE@EXAMPLE.COM") #?create_v2102print(f"Username: {user_v2.usernamealice}")  # Same interface!103print(f"Email: {user_v2.emailalice@example.com}")  # But normalized!
    outputUsername: alice
  11. def email(self): #?email_getter_v2

    32@property #?email_property_v233def email(self⟨UserV2 B⟩): #?email_getter_v234    """Get email."""35    return self._emailalice@example.com #?return_email_v2
  12. print(f"Email: {user_v2.email}") # But normalized!

    102print(f"Username: {user_v2.username}")  # Same interface!103print(f"Email: {user_v2.emailalice@example.com}")  # But normalized!
    outputEmail: alice@example.com
  13. def username(self, value): #?username_setter_method_v2

    pass 2 of 2
    25@username.setter #?username_setter_v226def username(self⟨UserV2 B⟩, valueab): #?username_setter_method_v227    """Set username with validation (NEW!)."""28    if not isinstance(value, str) or len(value) < 3: #?validate_username_v2
  14. if not isinstance(value, str) or len(value) < 3: #?validate_username_v…

    27"""Set username with validation (NEW!)."""28if not isinstance(valueab, str) or len(value) < 3: #?validate_username_v229    raise ValueError("Username must be string with 3+ chars") #?raise_username_v230self._username = value #?store_username_v2
  15. except ValueError as e: #?catch_v2_error

    107    user_v2.username = "ab"  # Now validated!108except ValueError as e: #?catch_v2_error109    print(f"Validation error: {eUsername must be string with 3+ chars}") #?print_v2_error110111# Legacy cart problems #?legacy_cart_demo
    outputValidation error: Username must be string with 3+ chars
    Validation error: Username must be string with 3+ chars
  16. print(" --- Legacy Cart (Manual Total) ---")

    111# Legacy cart problems #?legacy_cart_demo112print("\n--- Legacy Cart (Manual Total) ---")113legacy = LegacyCart() #?create_legacy114legacy.add_item(10.00) #?add_legacy_1
    output
    --- Legacy Cart (Manual Total) ---
  17. self.items ← [], self.total ← 0

    49def __init__(self⟨LegacyCart C⟩): #?init_legacy_cart50    self.items→ [] = [] #?direct_items51    self.total→ 0 = 0 #?direct_total
  18. legacy ← ⟨LegacyCart C⟩

    112print("\n--- Legacy Cart (Manual Total) ---")113legacy→ ⟨LegacyCart C⟩ = LegacyCart() #?create_legacy114legacy⟨LegacyCart C⟩.add_item(10.00) #?add_legacy_1115legacy.add_item(20.00) #?add_legacy_2
  19. self.items ← [10.0]

    pass 1 of 2
    53def add_item(self⟨LegacyCart C⟩, price10.0): #?add_item_legacy54    """Add item (user must update total manually - error-prone!)."""55    self.items→ [10.0].append(price10.0) #?append_item56    # User responsible for updating total! #?manual_total
  20. legacy.add_item(10.00) #?add_legacy_1

    113legacy = LegacyCart() #?create_legacy114legacy⟨LegacyCart C⟩.add_item(10.00) #?add_legacy_1115legacy⟨LegacyCart C⟩.add_item(20.00) #?add_legacy_2116print(f"Items: {legacy.items}") #?print_legacy_items
  21. self.items ← [10.0, 20.0]

    pass 2 of 2
    53def add_item(self⟨LegacyCart C⟩, price20.0): #?add_item_legacy54    """Add item (user must update total manually - error-prone!)."""55    self.items→ [10.0, 20.0].append(price20.0) #?append_item56    # User responsible for updating total! #?manual_total
  22. legacy.total ← 30.0

    114legacy.add_item(10.00) #?add_legacy_1115legacy⟨LegacyCart C⟩.add_item(20.00) #?add_legacy_2116print(f"Items: {legacy.items[10.0, 20.0]}") #?print_legacy_items117print(f"Total: ${legacy.total0}")  # Still 0 - forgot to update!118119# Forgot to update total! #?forgot_total120legacy.total→ 30.0 = 30.00  # Manual and error-prone121print(f"After manual update: ${legacy.total30.0}") #?print_manual_total122123# Modern cart with computed property #?modern_cart_demo124print("\n--- Modern Cart (Computed Total) ---")125modern = ModernCart() #?create_modern126modern.add_item(10.00) #?add_modern_1
    outputItems: [10.0, 20.0]
    Total: $0
    After manual update: $30.0
    
    --- Modern Cart (Computed Total) ---
  23. self._items ← []

    63def __init__(self⟨ModernCart D⟩): #?init_modern_cart64    self._items→ [] = [] #?private_items
  24. modern ← ⟨ModernCart D⟩

    124print("\n--- Modern Cart (Computed Total) ---")125modern→ ⟨ModernCart D⟩ = ModernCart() #?create_modern126modern⟨ModernCart D⟩.add_item(10.00) #?add_modern_1127modern.add_item(20.00) #?add_modern_2
  25. self._items ← [10.0]

    pass 1 of 3
    76def add_item(self⟨ModernCart D⟩, price10.0): #?add_item_modern77    """Add item (total auto-updates!)."""78    if price < 0: #?validate_price79        raise ValueError("Price cannot be negative") #?raise_price80    self._items→ [10.0].append(price10.0) #?append_modern_item81    # No manual total update needed! #?auto_total
    All 3 passes — pass 1 is the card above
    passpriceself._items
    110.0[] [10.0]
    220.0[10.0] [10.0, 20.0]
    315.0[10.0, 20.0] [10.0, 20.0, 15.0]
  26. modern.add_item(10.00) #?add_modern_1

    125modern = ModernCart() #?create_modern126modern⟨ModernCart D⟩.add_item(10.00) #?add_modern_1127modern⟨ModernCart D⟩.add_item(20.00) #?add_modern_2128print(f"Items: {modern.items}") #?print_modern_items
  27. modern.add_item(20.00) #?add_modern_2

    126modern.add_item(10.00) #?add_modern_1127modern⟨ModernCart D⟩.add_item(20.00) #?add_modern_2128print(f"Items: {modern.items[10.0, 20.0]}") #?print_modern_items129print(f"Total: ${modern.total}")  # Always correct!
  28. def items(self): #?items_getter

    66@property #?items_property67def items(self⟨ModernCart D⟩): #?items_getter68    """Get items (read-only copy)."""69    return self._items[10.0, 20.0].copy() #?return_items_copy
  29. print(f"Items: {modern.items}") #?print_modern_items

    127modern.add_item(20.00) #?add_modern_2128print(f"Items: {modern.items[10.0, 20.0]}") #?print_modern_items129print(f"Total: ${modern.total30.0}")  # Always correct!
    outputItems: [10.0, 20.0]
  30. def total(self): #?total_getter

    pass 1 of 2
    71@property #?total_property72def total(self⟨ModernCart D⟩): #?total_getter73    """Computed total (always accurate!)."""74    return sum(self._items[10.0, 20.0]) #?calc_total
  31. print(f"Total: ${modern.total}") # Always correct!

    128print(f"Items: {modern.items}") #?print_modern_items129print(f"Total: ${modern.total30.0}")  # Always correct!130131modern⟨ModernCart D⟩.add_item(15.00) #?add_modern_3132print(f"After adding item: ${modern.total}")  # Auto-updated!
    outputTotal: $30.0
  32. modern.add_item(15.00) #?add_modern_3

    131modern⟨ModernCart D⟩.add_item(15.00) #?add_modern_3132print(f"After adding item: ${modern.total45.0}")  # Auto-updated!
  33. def total(self): #?total_getter

    pass 2 of 2
    71@property #?total_property72def total(self⟨ModernCart D⟩): #?total_getter73    """Computed total (always accurate!)."""74    return sum(self._items[10.0, 20.0, 15.0]) #?calc_total
  34. print(f"After adding item: ${modern.total}") # Auto-updated!

    131modern.add_item(15.00) #?add_modern_3132print(f"After adding item: ${modern.total45.0}")  # Auto-updated!
    outputAfter adding item: $45.0
  35. except AttributeError as e: #?catch_total_error

    136    modern.total = 9999 #?attempt_set_total137except AttributeError as e: #?catch_total_error138    print(f"Error: {eproperty 'total' of 'ModernCart' object has no setter}") #?print_total_error139140print("\n=== Migration Benefits ===")
    outputError: property 'total' of 'ModernCart' object has no setter
    Error: property 'total' of 'ModernCart' object has no setter
  36. print(" === Migration Benefits ===")

    140    print("\n=== Migration Benefits ===")141    print("""1421. Backward compatible - same interface1432. Add validation without breaking existing code1443. Transform data (e.g., normalize email)1454. Computed values always accurate1465. Prevent incorrect manual updates1476. Read-only computed properties148    """)
    output
    === Migration Benefits ===
    
    1. Backward compatible - same interface
    2. Add validation without breaking existing code
    3. Transform data (e.g., normalize email)
    4. Computed values always accurate
    5. Prevent incorrect manual updates
    6. Read-only computed properties
        
  37. main()

    151if __name__ == "__main__":152    main()

Start with plain attribute. Add property later. Callers don't change.

Exercise: practical.py

Build a temperature class with Celsius/Fahrenheit properties