Your BankAccount class has a balance. If code can set account.balance = -1000 directly, bugs happen. Encapsulation hides internal data and provides controlled access through methods and properties - protecting your data from misuse.

Public vs private conventions

Python uses naming conventions for access control.

public_vs_private.py
Replay: real traced execution (multi-file project)
# Public vs Private Attributes

print("=== Public Attributes ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

buddy = Dog("Buddy", 3)

# Direct access - anyone can read/write
print(f"Name: {buddy.name}")
buddy.age = 4  # Changed directly
print(f"Age: {buddy.age}")

# Problem: No validation!
buddy.age = -5  # Oops! Invalid value
print(f"Invalid age: {buddy.age}")

print("\n=== Protected Attributes (Convention) ===")

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

    def get_info(self):
        return f"{self._name} is {self._age} years old"

whiskers = Cat("Whiskers", 5)
print(whiskers.get_info())

# Still accessible, but indicates "internal use"
print(f"Direct access: {whiskers._name}")  # Works, but discouraged
whiskers._age = 6  # Works, but breaks convention

print("\n=== Private Attributes (Name Mangling) ===")

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance

    def get_balance(self):
        return self.__balance

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return True
        return False

account = BankAccount(1000)
print(f"Balance: {account.get_balance()}")

# Can't access directly
# print(account.__balance)  # AttributeError!

# Name mangling - still accessible but harder
print(f"Mangled name: {account._BankAccount__balance}")  # Works!

print("\n=== Why Use Privacy? ===")

class Counter:
    def __init__(self):
        self.__count = 0
        self.__min = 0
        self.__max = 100

    def increment(self):
        if self.__count < self.__max:
            self.__count += 1

    def decrement(self):
        if self.__count > self.__min:
            self.__count -= 1

    def value(self):
        return self.__count

counter = Counter()
counter.increment()
counter.increment()
counter.increment()
print(f"Count: {counter.value()}")

# Can't bypass validation
# counter.__count = 999  # Creates new attribute, not the real one!
counter.increment()
print(f"Count: {counter.value()}")  # Still 4, not 999

print("\n=== Access Summary ===")

class Example:
    def __init__(self):
        self.public = "Anyone can access"
        self._protected = "Convention: internal"
        self.__private = "Name mangled"

obj = Example()
print(f"public: {obj.public}")
print(f"_protected: {obj._protected}")  # Accessible
# print(f"__private: {obj.__private}")  # Error!
print(f"_Example__private: {obj._Example__private}")  # Name mangled
  1. print("=== Public Attributes === ")

    3print("=== Public Attributes ===\n")45class Dog:6    def __init__(self, name, age):7        self.name = name  #?public8        self.age = age910buddy = Dog("Buddy", 3)
    output=== Public Attributes ===
  2. self.name ← Buddy, self.age ← 3

    5class Dog:6    def __init__(self⟨Dog A⟩, nameBuddy, age3):7        self.name→ Buddy = nameBuddy  #?public8        self.age→ 3 = age3
  3. buddy ← ⟨Dog A⟩, buddy.age ← 4

    10buddy→ ⟨Dog A⟩ = Dog("Buddy", 3)1112# Direct access - anyone can read/write  #?directaccess13print(f"Name: {buddy.nameBuddy}")14buddy.age→ 4 = 4  # Changed directly15print(f"Age: {buddy.age4}")1617# Problem: No validation!  #?novalid18buddy.age→ -5 = -5  # Oops! Invalid value19print(f"Invalid age: {buddy.age-5}")2021print("\n=== Protected Attributes (Convention) ===")2223class Cat:24    def __init__(self, name, age):25        self._name = name  #?protected26        self._age = age27    28    def get_info(self):29        return f"{self._name} is {self._age} years old"3031whiskers = Cat("Whiskers", 5)32print(whiskers.get_info())
    outputName: Buddy
    Age: 4
    Invalid age: -5
    
    === Protected Attributes (Convention) ===
  4. self._name ← Whiskers, self._age ← 5

    23class Cat:24    def __init__(self⟨Cat B⟩, nameWhiskers, age5):25        self._name→ Whiskers = nameWhiskers  #?protected26        self._age→ 5 = age5
  5. whiskers ← ⟨Cat B⟩

    31whiskers→ ⟨Cat B⟩ = Cat("Whiskers", 5)32print(whiskers⟨Cat B⟩.get_info())
  6. def get_info(self):

    28def get_info(self⟨Cat B⟩):29    return f"{self._nameWhiskers} is {self._age5} years old"
  7. whiskers._age ← 6

    31whiskers = Cat("Whiskers", 5)32print(whiskers⟨Cat B⟩.get_info())3334# Still accessible, but indicates "internal use"  #?stillaccessible35print(f"Direct access: {whiskers._nameWhiskers}")  # Works, but discouraged36whiskers._age→ 6 = 6  # Works, but breaks convention3738print("\n=== Private Attributes (Name Mangling) ===")3940class BankAccount:41    def __init__(self, balance):42        self.__balance = balance  #?private43    44    def get_balance(self):  #?getter45        return self.__balance46    47    def deposit(self, amount):  #?deposit48        if amount > 0:49            self.__balance += amount50            return True51        return False5253account = BankAccount(1000)54print(f"Balance: {account.get_balance()}")
    outputWhiskers is 5 years old
    Direct access: Whiskers
    
    === Private Attributes (Name Mangling) ===
  8. self.__balance ← 1000

    40class BankAccount:41    def __init__(self⟨BankAccount C⟩, balance1000):42        self.__balance→ 1000 = balance1000  #?private
  9. account ← ⟨BankAccount C⟩

    53account→ ⟨BankAccount C⟩ = BankAccount(1000)54print(f"Balance: {account⟨BankAccount C⟩.get_balance()}")
  10. def get_balance(self): #?getter

    44def get_balance(self⟨BankAccount C⟩):  #?getter45    return self.__balance1000
  11. print(f"Balance: {account.get_balance()}")

    53account = BankAccount(1000)54print(f"Balance: {account⟨BankAccount C⟩.get_balance()}")5556# Can't access directly  #?cantaccess57# print(account.__balance)  # AttributeError!5859# Name mangling - still accessible but harder  #?mangling60print(f"Mangled name: {account._BankAccount__balance1000}")  # Works!6162print("\n=== Why Use Privacy? ===")6364class Counter:65    def __init__(self):66        self.__count = 0  #?whyprivate67        self.__min = 068        self.__max = 10069    70    def increment(self):  #?controlled71        if self.__count < self.__max:72            self.__count += 173    74    def decrement(self):75        if self.__count > self.__min:76            self.__count -= 177    78    def value(self):79        return self.__count8081counter = Counter()82counter.increment()
    outputBalance: 1000
    Mangled name: 1000
    
    === Why Use Privacy? ===
  12. self.__count ← 0, self.__min ← 0, self.__max ← 100

    64class Counter:65    def __init__(self⟨Counter D⟩):66        self.__count→ 0 = 0  #?whyprivate67        self.__min→ 0 = 068        self.__max→ 100 = 100
  13. counter ← ⟨Counter D⟩

    81counter→ ⟨Counter D⟩ = Counter()82counter⟨Counter D⟩.increment()83counter.increment()
  14. def increment(self): #?controlled

    pass 1 of 4
    70def increment(self⟨Counter D⟩):  #?controlled71    if self.__count < self.__max:72        self.__count += 1
  15. self.__count ← 1

    pass 1 of 4
    70def increment(self):  #?controlled71    if self.__count0 < self.__max100:72        self.__count→ 1 += 1
    All 4 passes — pass 1 is the card above
    passself.__count
    10 1
    21 2
    32 3
    43 4
  16. counter.increment()

    81counter = Counter()82counter⟨Counter D⟩.increment()83counter⟨Counter D⟩.increment()84counter.increment()
  17. counter.increment()

    82counter.increment()83counter⟨Counter D⟩.increment()84counter⟨Counter D⟩.increment()85print(f"Count: {counter.value()}")
  18. counter.increment()

    83counter.increment()84counter⟨Counter D⟩.increment()85print(f"Count: {counter⟨Counter D⟩.value()}")
  19. def value(self):

    pass 1 of 2
    78def value(self⟨Counter D⟩):79    return self.__count3
  20. print(f"Count: {counter.value()}")

    84counter.increment()85print(f"Count: {counter⟨Counter D⟩.value()}")8687# Can't bypass validation  #?cantbypass88# counter.__count = 999  # Creates new attribute, not the real one!89counter⟨Counter D⟩.increment()90print(f"Count: {counter.value()}")  # Still 4, not 999
    outputCount: 3
  21. counter.increment()

    88# counter.__count = 999  # Creates new attribute, not the real one!89counter⟨Counter D⟩.increment()90print(f"Count: {counter⟨Counter D⟩.value()}")  # Still 4, not 999
  22. def value(self):

    pass 2 of 2
    78def value(self⟨Counter D⟩):79    return self.__count4
  23. print(f"Count: {counter.value()}") # Still 4, not 999

    89counter.increment()90print(f"Count: {counter⟨Counter D⟩.value()}")  # Still 4, not 9999192print("\n=== Access Summary ===")9394class Example:95    def __init__(self):96        self.public = "Anyone can access"  #?summary97        self._protected = "Convention: internal"98        self.__private = "Name mangled"99100obj = Example()101print(f"public: {obj.public}")
    outputCount: 4
    
    === Access Summary ===
  24. self.public ← Anyone can access, self._protected ← Convention: internal

    94class Example:95    def __init__(self⟨Example E⟩):96        self.public→ Anyone can access = "Anyone can access"  #?summary97        self._protected→ Convention: internal = "Convention: internal"98        self.__private→ Name mangled = "Name mangled"
  25. obj ← ⟨Example E⟩

    100obj→ ⟨Example E⟩ = Example()101print(f"public: {obj.publicAnyone can access}")102print(f"_protected: {obj._protectedConvention: internal}")  # Accessible103# print(f"__private: {obj.__private}")  # Error!104print(f"_Example__private: {obj._Example__privateName mangled}")  # Name mangled105#@help public
    outputpublic: Anyone can access
    _protected: Convention: internal
    _Example__private: Name mangled

_name means "internal". __name means "strongly private" (name mangling).

_underscore Single underscore prefix: convention for internal use. Not enforced.
__dunder Double underscore prefix: name mangling. `__attr` becomes `_ClassName__attr`.

Traditional getters and setters

Methods to read and write private data.

getters_setters.py
Replay: real traced execution (multi-file project)
# Traditional Getters and Setters

print("=== Basic Getter and Setter ===\n")

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

    def get_name(self):
        return self._name

    def set_name(self, name):
        if len(name) >= 2:
            self._name = name
        else:
            print("Name too short!")

    def get_age(self):
        return self._age

    def set_age(self, age):
        if 0 <= age <= 150:
            self._age = age
        else:
            print(f"Invalid age: {age}")

person = Person("Alice", 30)

# Using getters
print(f"Name: {person.get_name()}")
print(f"Age: {person.get_age()}")

# Using setters
person.set_name("Bob")
person.set_age(25)
print(f"Updated: {person.get_name()}, {person.get_age()}")

# Validation prevents bad values
person.set_age(-5)  # Rejected
person.set_name("X")  # Rejected
print(f"Still: {person.get_name()}, {person.get_age()}")

print("\n=== Getter with Logic ===")

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    def get_celsius(self):
        return self._celsius

    def get_fahrenheit(self):
        return self._celsius * 9/5 + 32

    def get_kelvin(self):
        return self._celsius + 273.15

    def set_celsius(self, value):
        if value >= -273.15:  # Absolute zero
            self._celsius = value
        else:
            print("Below absolute zero!")

temp = Temperature(25)
print(f"Celsius: {temp.get_celsius()}")
print(f"Fahrenheit: {temp.get_fahrenheit()}")
print(f"Kelvin: {temp.get_kelvin()}")

print("\n=== Setter with Side Effects ===")

class Logger:
    def __init__(self):
        self._log_level = "INFO"
        self._changes = []

    def get_log_level(self):
        return self._log_level

    def set_log_level(self, level):
        valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR"]
        if level.upper() in valid_levels:
            old = self._log_level
            self._log_level = level.upper()
            self._changes.append(f"{old} -> {self._log_level}")
            print(f"Log level changed: {old} -> {self._log_level}")
        else:
            print(f"Invalid level: {level}")

    def get_history(self):
        return self._changes.copy()

logger = Logger()
logger.set_log_level("DEBUG")
logger.set_log_level("ERROR")
logger.set_log_level("invalid")  # Rejected
print(f"History: {logger.get_history()}")

print("\n=== Why This Style is Verbose ===")

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

    def get_x(self): return self._x
    def set_x(self, x): self._x = x
    def get_y(self): return self._y
    def set_y(self, y): self._y = y

p = Point(3, 4)
# Verbose usage:
x = p.get_x()
y = p.get_y()
p.set_x(x + 1)
print(f"Point: ({p.get_x()}, {p.get_y()})")

print("""
Traditional getters/setters are verbose:
  p.get_x()     instead of  p.x
  p.set_x(5)    instead of  p.x = 5

Python offers @property for cleaner syntax!
""")
  1. print("=== Basic Getter and Setter === ")

    3print("=== Basic Getter and Setter ===\n")45class Person:6    def __init__(self, name, age):7        self._name = name8        self._age = age9    10    def get_name(self):  #?getter11        return self._name12    13    def set_name(self, name):  #?setter14        if len(name) >= 2:15            self._name = name16        else:17            print("Name too short!")18    19    def get_age(self):  #?ageget20        return self._age21    22    def set_age(self, age):  #?ageset23        if 0 <= age <= 150:24            self._age = age25        else:26            print(f"Invalid age: {age}")2728person = Person("Alice", 30)
    output=== Basic Getter and Setter ===
  2. self._name ← Alice, self._age ← 30

    5class Person:6    def __init__(self⟨Person A⟩, nameAlice, age30):7        self._name→ Alice = nameAlice8        self._age→ 30 = age30
  3. person ← ⟨Person A⟩

    28person→ ⟨Person A⟩ = Person("Alice", 30)2930# Using getters  #?useget31print(f"Name: {person⟨Person A⟩.get_name()}")32print(f"Age: {person.get_age()}")
  4. def get_name(self): #?getter

    pass 1 of 3
    10def get_name(self⟨Person A⟩):  #?getter11    return self._nameAlice
    All 3 passes — pass 1 is the card above
    passself._name
    1Alice
    2Bob
    3Bob
  5. print(f"Name: {person.get_name()}")

    30# Using getters  #?useget31print(f"Name: {person⟨Person A⟩.get_name()}")32print(f"Age: {person⟨Person A⟩.get_age()}")
    outputName: Alice
  6. def get_age(self): #?ageget

    pass 1 of 3
    19def get_age(self⟨Person A⟩):  #?ageget20    return self._age30
    All 3 passes — pass 1 is the card above
    passself._age
    130
    225
    325
  7. print(f"Age: {person.get_age()}")

    31print(f"Name: {person.get_name()}")32print(f"Age: {person⟨Person A⟩.get_age()}")3334# Using setters  #?useset35person⟨Person A⟩.set_name("Bob")36person.set_age(25)
    outputAge: 30
  8. def set_name(self, name): #?setter

    pass 1 of 2
    13def set_name(self⟨Person A⟩, nameBob):  #?setter14    if len(name) >= 2:15        self._name = name
  9. self._name ← Bob

    13def set_name(self, name):  #?setter14    if len(nameBob) >= 2:15        self._name→ Bob = nameBob16    else:
  10. person.set_name("Bob")

    34# Using setters  #?useset35person⟨Person A⟩.set_name("Bob")36person⟨Person A⟩.set_age(25)37print(f"Updated: {person.get_name()}, {person.get_age()}")
  11. def set_age(self, age): #?ageset

    pass 1 of 2
    22def set_age(self⟨Person A⟩, age25):  #?ageset23    if 0 <= age <= 150:24        self._age = age
  12. self._age ← 25

    22def set_age(self, age):  #?ageset23    if 0 <= age25 <= 150:24        self._age→ 25 = age2525    else:
  13. person.set_age(25)

    35person.set_name("Bob")36person⟨Person A⟩.set_age(25)37print(f"Updated: {person⟨Person A⟩.get_name()}, {person.get_age()}")
  14. print(f"Updated: {person.get_name()}, {person.get_age()}")

    36person.set_age(25)37print(f"Updated: {person⟨Person A⟩.get_name()}, {person.get_age()}")3839# Validation prevents bad values  #?validation40person⟨Person A⟩.set_age(-5)  # Rejected41person.set_name("X")  # Rejected
    outputUpdated: Bob, 25
  15. def set_age(self, age): #?ageset

    pass 2 of 2
    22def set_age(self⟨Person A⟩, age-5):  #?ageset23    if 0 <= age <= 150:24        self._age = age
  16. else:

    23if 0 <= age <= 150:24    self._age = age25else:26    print(f"Invalid age: {age-5}")
    outputInvalid age: -5
  17. person.set_age(-5) # Rejected

    39# Validation prevents bad values  #?validation40person⟨Person A⟩.set_age(-5)  # Rejected41person⟨Person A⟩.set_name("X")  # Rejected42print(f"Still: {person.get_name()}, {person.get_age()}")
  18. def set_name(self, name): #?setter

    pass 2 of 2
    13def set_name(self⟨Person A⟩, nameX):  #?setter14    if len(name) >= 2:15        self._name = name
  19. else:

    14if len(name) >= 2:15    self._name = name16else:17    print("Name too short!")
    outputName too short!
  20. person.set_name("X") # Rejected

    40person.set_age(-5)  # Rejected41person⟨Person A⟩.set_name("X")  # Rejected42print(f"Still: {person⟨Person A⟩.get_name()}, {person.get_age()}")
  21. print(f"Still: {person.get_name()}, {person.get_age()}")

    41person.set_name("X")  # Rejected42print(f"Still: {person⟨Person A⟩.get_name()}, {person.get_age()}")4344print("\n=== Getter with Logic ===")4546class Temperature:47    def __init__(self, celsius):48        self._celsius = celsius49    50    def get_celsius(self):51        return self._celsius52    53    def get_fahrenheit(self):  #?computedgetter54        return self._celsius * 9/5 + 3255    56    def get_kelvin(self):57        return self._celsius + 273.1558    59    def set_celsius(self, value):60        if value >= -273.15:  # Absolute zero  #?physicscheck61            self._celsius = value62        else:63            print("Below absolute zero!")6465temp = Temperature(25)66print(f"Celsius: {temp.get_celsius()}")
    outputStill: Bob, 25
    
    === Getter with Logic ===
  22. self._celsius ← 25

    46class Temperature:47    def __init__(self⟨Temperature B⟩, celsius25):48        self._celsius→ 25 = celsius25
  23. temp ← ⟨Temperature B⟩

    65temp→ ⟨Temperature B⟩ = Temperature(25)66print(f"Celsius: {temp⟨Temperature B⟩.get_celsius()}")67print(f"Fahrenheit: {temp.get_fahrenheit()}")
  24. def get_celsius(self):

    50def get_celsius(self⟨Temperature B⟩):51    return self._celsius25
  25. print(f"Celsius: {temp.get_celsius()}")

    65temp = Temperature(25)66print(f"Celsius: {temp⟨Temperature B⟩.get_celsius()}")67print(f"Fahrenheit: {temp⟨Temperature B⟩.get_fahrenheit()}")68print(f"Kelvin: {temp.get_kelvin()}")
    outputCelsius: 25
  26. def get_fahrenheit(self): #?computedgetter

    53def get_fahrenheit(self⟨Temperature B⟩):  #?computedgetter54    return self._celsius25 * 9/5 + 32
  27. print(f"Fahrenheit: {temp.get_fahrenheit()}")

    66print(f"Celsius: {temp.get_celsius()}")67print(f"Fahrenheit: {temp⟨Temperature B⟩.get_fahrenheit()}")68print(f"Kelvin: {temp⟨Temperature B⟩.get_kelvin()}")
    outputFahrenheit: 77.0
  28. def get_kelvin(self):

    56def get_kelvin(self⟨Temperature B⟩):57    return self._celsius25 + 273.15
  29. print(f"Kelvin: {temp.get_kelvin()}")

    67print(f"Fahrenheit: {temp.get_fahrenheit()}")68print(f"Kelvin: {temp⟨Temperature B⟩.get_kelvin()}")6970print("\n=== Setter with Side Effects ===")7172class Logger:73    def __init__(self):74        self._log_level = "INFO"75        self._changes = []76    77    def get_log_level(self):78        return self._log_level79    80    def set_log_level(self, level):  #?sideeffect81        valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR"]82        if level.upper() in valid_levels:83            old = self._log_level84            self._log_level = level.upper()85            self._changes.append(f"{old} -> {self._log_level}")  #?trackchange86            print(f"Log level changed: {old} -> {self._log_level}")87        else:88            print(f"Invalid level: {level}")89    90    def get_history(self):  #?history91        return self._changes.copy()9293logger = Logger()94logger.set_log_level("DEBUG")
    outputKelvin: 298.15
    
    === Setter with Side Effects ===
  30. self._log_level ← INFO, self._changes ← []

    72class Logger:73    def __init__(self⟨Logger C⟩):74        self._log_level→ INFO = "INFO"75        self._changes→ [] = []
  31. logger ← ⟨Logger C⟩

    93logger→ ⟨Logger C⟩ = Logger()94logger⟨Logger C⟩.set_log_level("DEBUG")95logger.set_log_level("ERROR")
  32. valid_levels ← ['DEBUG', 'INFO', 'WARNING', 'ERROR']

    pass 1 of 3
    80def set_log_level(self⟨Logger C⟩, levelDEBUG):  #?sideeffect81    valid_levels→ ['DEBUG', 'INFO', 'WARNING', 'ERROR'] = ["DEBUG", "INFO", "WARNING", "ERROR"]82    if level.upper() in valid_levels:
    All 3 passes — pass 1 is the card above
    passlevelvalid_levelsoldself._log_levelself._changes
    1DEBUG['DEBUG', 'INFO', 'WARNING', 'ERROR']INFOINFO DEBUG[] ['INFO -> DEBUG']
    2ERROR['DEBUG', 'INFO', 'WARNING', 'ERROR']DEBUGDEBUG ERROR['INFO -> DEBUG'] ['INFO -> DEBUG', 'DEBUG -> ERROR']
    3invalid['DEBUG', 'INFO', 'WARNING', 'ERROR']
  33. old ← INFO, self._log_level ← DEBUG, self._changes ← ['INFO -> DEBUG']

    pass 1 of 2
    81valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR"]82if levelDEBUG.upper() in valid_levels['DEBUG', 'INFO', 'WARNING', 'ERROR']:83    old→ INFO = self._log_levelINFO84    self._log_level→ DEBUG = levelDEBUG.upper()85    self._changes→ ['INFO -> DEBUG'].append(f"{oldINFO} -> {self._log_levelDEBUG}")  #?trackchange86    print(f"Log level changed: {oldINFO} -> {self._log_levelDEBUG}")87else:
    outputLog level changed: INFO -> DEBUG
  34. logger.set_log_level("DEBUG")

    93logger = Logger()94logger⟨Logger C⟩.set_log_level("DEBUG")95logger⟨Logger C⟩.set_log_level("ERROR")96logger.set_log_level("invalid")  # Rejected
  35. old ← DEBUG, self._log_level ← ERROR, self._changes ← ['INFO -> DEBUG', 'DEBUG -> ERROR']

    pass 2 of 2
    81valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR"]82if levelERROR.upper() in valid_levels['DEBUG', 'INFO', 'WARNING', 'ERROR']:83    old→ DEBUG = self._log_levelDEBUG84    self._log_level→ ERROR = levelERROR.upper()85    self._changes→ ['INFO -> DEBUG', 'DEBUG -> ERROR'].append(f"{oldDEBUG} -> {self._log_levelERROR}")  #?trackchange86    print(f"Log level changed: {oldDEBUG} -> {self._log_levelERROR}")87else:
    outputLog level changed: DEBUG -> ERROR
  36. logger.set_log_level("ERROR")

    94logger.set_log_level("DEBUG")95logger⟨Logger C⟩.set_log_level("ERROR")96logger⟨Logger C⟩.set_log_level("invalid")  # Rejected97print(f"History: {logger.get_history()}")
  37. else:

    85    self._changes.append(f"{old} -> {self._log_level}")  #?trackchange86    print(f"Log level changed: {old} -> {self._log_level}")87else:88    print(f"Invalid level: {levelinvalid}")
    outputInvalid level: invalid
  38. logger.set_log_level("invalid") # Rejected

    95logger.set_log_level("ERROR")96logger⟨Logger C⟩.set_log_level("invalid")  # Rejected97print(f"History: {logger⟨Logger C⟩.get_history()}")
  39. def get_history(self): #?history

    90def get_history(self⟨Logger C⟩):  #?history91    return self._changes['INFO -> DEBUG', 'DEBUG -> ERROR'].copy()
  40. print(f"History: {logger.get_history()}")

    96logger.set_log_level("invalid")  # Rejected97print(f"History: {logger⟨Logger C⟩.get_history()}")9899print("\n=== Why This Style is Verbose ===")100101class Point:102    def __init__(self, x, y):103        self._x = x104        self._y = y105    106    def get_x(self): return self._x107    def set_x(self, x): self._x = x108    def get_y(self): return self._y109    def set_y(self, y): self._y = y110111p = Point(3, 4)112# Verbose usage:  #?verbose
    outputHistory: ['INFO -> DEBUG', 'DEBUG -> ERROR']
    
    === Why This Style is Verbose ===
  41. self._x ← 3, self._y ← 4

    101class Point:102    def __init__(self⟨Point D⟩, x3, y4):103        self._x→ 3 = x3104        self._y→ 4 = y4
  42. p ← ⟨Point D⟩, x ← 3, y ← 4

    111p→ ⟨Point D⟩ = Point(3, 4)112# Verbose usage:  #?verbose113x→ 3 = p⟨Point D⟩.get_x()114y→ 4 = p⟨Point D⟩.get_y()115p⟨Point D⟩.set_x(x3 + 1)116print(f"Point: ({p⟨Point D⟩.get_x()}, {p.get_y()})")117118print("""119Traditional getters/setters are verbose:120  p.get_x()     instead of  p.x121  p.set_x(5)    instead of  p.x = 5122123Python offers @property for cleaner syntax!124""")125#@help getter
    outputPoint: (4, 4)
    
    Traditional getters/setters are verbose:
      p.get_x()     instead of  p.x
      p.set_x(5)    instead of  p.x = 5
    
    Python offers @property for cleaner syntax!

get_balance() and set_balance() control access. Java-style.

Python properties

The Pythonic way to control attribute access.

example
properties.py
Replay: real traced execution (multi-file project)
# Python Properties

print("=== Basic @property ===\n")

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

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

    @radius.setter
    def radius(self, value):
        """Set the radius with validation."""
        if value > 0:
            self._radius = value
        else:
            print(f"Invalid radius: {value}")

circle = Circle(5)

# Access like attribute
print(f"Radius: {circle.radius}")  # Calls getter

# Assign like attribute
circle.radius = 10  # Calls setter
print(f"New radius: {circle.radius}")

# Validation still works
circle.radius = -5  # Rejected
print(f"Still: {circle.radius}")

print("\n=== Properties vs Direct Access ===")

# Compare syntax:
print("""
Traditional:
  value = obj.get_attribute()
  obj.set_attribute(value)

With @property:
  value = obj.attribute
  obj.attribute = value

Same control, cleaner syntax!
""")

print("=== Multiple Properties ===")

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value > 0:
            self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value > 0:
            self._height = value

    @property
    def area(self):
        """Computed property - no setter."""
        return self._width * self._height

    @property
    def perimeter(self):
        return 2 * (self._width + self._height)

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

# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")  # Automatically updated!

print("\n=== Property with Deleter ===")

class User:
    def __init__(self, email):
        self._email = email

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

    @email.setter
    def email(self, value):
        if "@" in value:
            self._email = value

    @email.deleter
    def email(self):
        print("Removing email...")
        self._email = None

user = User("alice@example.com")
print(f"Email: {user.email}")

del user.email
print(f"After delete: {user.email}")

print("\n=== Property Naming Patterns ===")

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

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

    @celsius.setter
    def celsius(self, value):
        if value >= -273.15:
            self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9

temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")

temp.fahrenheit = 212  # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties

print("=== Basic @property ===\n")

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

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

    @radius.setter
    def radius(self, value):
        """Set the radius with validation."""
        if value > 0:
            self._radius = value
        else:
            print(f"Invalid radius: {value}")

circle = Circle(2)

# Access like attribute
print(f"Radius: {circle.radius}")  # Calls getter

# Assign like attribute
circle.radius = 10  # Calls setter
print(f"New radius: {circle.radius}")

# Validation still works
circle.radius = -5  # Rejected
print(f"Still: {circle.radius}")

print("\n=== Properties vs Direct Access ===")

# Compare syntax:
print("""
Traditional:
  value = obj.get_attribute()
  obj.set_attribute(value)

With @property:
  value = obj.attribute
  obj.attribute = value

Same control, cleaner syntax!
""")

print("=== Multiple Properties ===")

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value > 0:
            self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value > 0:
            self._height = value

    @property
    def area(self):
        """Computed property - no setter."""
        return self._width * self._height

    @property
    def perimeter(self):
        return 2 * (self._width + self._height)

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

# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")  # Automatically updated!

print("\n=== Property with Deleter ===")

class User:
    def __init__(self, email):
        self._email = email

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

    @email.setter
    def email(self, value):
        if "@" in value:
            self._email = value

    @email.deleter
    def email(self):
        print("Removing email...")
        self._email = None

user = User("alice@example.com")
print(f"Email: {user.email}")

del user.email
print(f"After delete: {user.email}")

print("\n=== Property Naming Patterns ===")

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

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

    @celsius.setter
    def celsius(self, value):
        if value >= -273.15:
            self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9

temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")

temp.fahrenheit = 212  # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties

print("=== Basic @property ===\n")

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

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

    @radius.setter
    def radius(self, value):
        """Set the radius with validation."""
        if value > 0:
            self._radius = value
        else:
            print(f"Invalid radius: {value}")

circle = Circle(12)

# Access like attribute
print(f"Radius: {circle.radius}")  # Calls getter

# Assign like attribute
circle.radius = 10  # Calls setter
print(f"New radius: {circle.radius}")

# Validation still works
circle.radius = -5  # Rejected
print(f"Still: {circle.radius}")

print("\n=== Properties vs Direct Access ===")

# Compare syntax:
print("""
Traditional:
  value = obj.get_attribute()
  obj.set_attribute(value)

With @property:
  value = obj.attribute
  obj.attribute = value

Same control, cleaner syntax!
""")

print("=== Multiple Properties ===")

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value > 0:
            self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value > 0:
            self._height = value

    @property
    def area(self):
        """Computed property - no setter."""
        return self._width * self._height

    @property
    def perimeter(self):
        return 2 * (self._width + self._height)

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

# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")  # Automatically updated!

print("\n=== Property with Deleter ===")

class User:
    def __init__(self, email):
        self._email = email

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

    @email.setter
    def email(self, value):
        if "@" in value:
            self._email = value

    @email.deleter
    def email(self):
        print("Removing email...")
        self._email = None

user = User("alice@example.com")
print(f"Email: {user.email}")

del user.email
print(f"After delete: {user.email}")

print("\n=== Property Naming Patterns ===")

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

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

    @celsius.setter
    def celsius(self, value):
        if value >= -273.15:
            self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9

temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")

temp.fahrenheit = 212  # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties

print("=== Basic @property ===\n")

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

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

    @radius.setter
    def radius(self, value):
        """Set the radius with validation."""
        if value > 0:
            self._radius = value
        else:
            print(f"Invalid radius: {value}")

circle = Circle(5)

# Access like attribute
print(f"Radius: {circle.radius}")  # Calls getter

# Assign like attribute
circle.radius = 10  # Calls setter
print(f"New radius: {circle.radius}")

# Validation still works
circle.radius = -5  # Rejected
print(f"Still: {circle.radius}")

print("\n=== Properties vs Direct Access ===")

# Compare syntax:
print("""
Traditional:
  value = obj.get_attribute()
  obj.set_attribute(value)

With @property:
  value = obj.attribute
  obj.attribute = value

Same control, cleaner syntax!
""")

print("=== Multiple Properties ===")

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value > 0:
            self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value > 0:
            self._height = value

    @property
    def area(self):
        """Computed property - no setter."""
        return self._width * self._height

    @property
    def perimeter(self):
        return 2 * (self._width + self._height)

rect = Rectangle(4, 4)
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")

# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")  # Automatically updated!

print("\n=== Property with Deleter ===")

class User:
    def __init__(self, email):
        self._email = email

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

    @email.setter
    def email(self, value):
        if "@" in value:
            self._email = value

    @email.deleter
    def email(self):
        print("Removing email...")
        self._email = None

user = User("alice@example.com")
print(f"Email: {user.email}")

del user.email
print(f"After delete: {user.email}")

print("\n=== Property Naming Patterns ===")

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

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

    @celsius.setter
    def celsius(self, value):
        if value >= -273.15:
            self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9

temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")

temp.fahrenheit = 212  # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties

print("=== Basic @property ===\n")

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

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

    @radius.setter
    def radius(self, value):
        """Set the radius with validation."""
        if value > 0:
            self._radius = value
        else:
            print(f"Invalid radius: {value}")

circle = Circle(5)

# Access like attribute
print(f"Radius: {circle.radius}")  # Calls getter

# Assign like attribute
circle.radius = 10  # Calls setter
print(f"New radius: {circle.radius}")

# Validation still works
circle.radius = -5  # Rejected
print(f"Still: {circle.radius}")

print("\n=== Properties vs Direct Access ===")

# Compare syntax:
print("""
Traditional:
  value = obj.get_attribute()
  obj.set_attribute(value)

With @property:
  value = obj.attribute
  obj.attribute = value

Same control, cleaner syntax!
""")

print("=== Multiple Properties ===")

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value > 0:
            self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value > 0:
            self._height = value

    @property
    def area(self):
        """Computed property - no setter."""
        return self._width * self._height

    @property
    def perimeter(self):
        return 2 * (self._width + self._height)

rect = Rectangle(8, 2)
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")

# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")  # Automatically updated!

print("\n=== Property with Deleter ===")

class User:
    def __init__(self, email):
        self._email = email

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

    @email.setter
    def email(self, value):
        if "@" in value:
            self._email = value

    @email.deleter
    def email(self):
        print("Removing email...")
        self._email = None

user = User("alice@example.com")
print(f"Email: {user.email}")

del user.email
print(f"After delete: {user.email}")

print("\n=== Property Naming Patterns ===")

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

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

    @celsius.setter
    def celsius(self, value):
        if value >= -273.15:
            self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9

temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")

temp.fahrenheit = 212  # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
  1. print("=== Basic @property === ")

    3print("=== Basic @property ===\n")45class Circle:6    def __init__(self, radius):7        self._radius = radius8    9    @property  #?propertydeco10    def radius(self):  #?propgetter11        """Get the radius."""12        return self._radius13    14    @radius.setter  #?propsetter15    def radius(self, value):16        """Set the radius with validation."""17        if value > 0:18            self._radius = value19        else:20            print(f"Invalid radius: {value}")2122circle = Circle(5)  #@circle=Circle(2), Circle(12)
    output=== Basic @property ===
  2. self._radius ← 5

    5class Circle:6    def __init__(self⟨Circle A⟩, radius5):7        self._radius→ 5 = radius5
  3. circle ← ⟨Circle A⟩

    22circle→ ⟨Circle A⟩ = Circle(5)  #@circle=Circle(2), Circle(12)2324# Access like attribute  #?accesslike25print(f"Radius: {circle.radius5}")  # Calls getter
  4. def radius(self): #?propgetter

    pass 1 of 3
    9@property  #?propertydeco10def radius(self⟨Circle A⟩):  #?propgetter11    """Get the radius."""12    return self._radius5
    All 3 passes — pass 1 is the card above
    passself._radius
    15
    210
    310
  5. print(f"Radius: {circle.radius}") # Calls getter

    24# Access like attribute  #?accesslike25print(f"Radius: {circle.radius5}")  # Calls getter2627# Assign like attribute  #?assignlike28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius}")
    outputRadius: 5
  6. def radius(self, value):

    pass 1 of 2
    14@radius.setter  #?propsetter15def radius(self⟨Circle A⟩, value10):16    """Set the radius with validation."""17    if value > 0:
  7. self._radius ← 10

    16"""Set the radius with validation."""17if value10 > 0:18    self._radius→ 10 = value1019else:
  8. circle.radius ← 10

    27# Assign like attribute  #?assignlike28circle.radius→ 10 = 10  # Calls setter29print(f"New radius: {circle.radius10}")
  9. print(f"New radius: {circle.radius}")

    28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius10}")3031# Validation still works  #?propvalid32circle.radius = -5  # Rejected33print(f"Still: {circle.radius}")
    outputNew radius: 10
  10. def radius(self, value):

    pass 2 of 2
    14@radius.setter  #?propsetter15def radius(self⟨Circle A⟩, value-5):16    """Set the radius with validation."""17    if value > 0:
  11. else:

    17if value > 0:18    self._radius = value19else:20    print(f"Invalid radius: {value-5}")
    outputInvalid radius: -5
  12. circle.radius ← 10

    31# Validation still works  #?propvalid32circle.radius→ 10 = -5  # Rejected33print(f"Still: {circle.radius10}")
  13. print(f"Still: {circle.radius}")

    32circle.radius = -5  # Rejected33print(f"Still: {circle.radius10}")3435print("\n=== Properties vs Direct Access ===")3637# Compare syntax:  #?compare38print("""39Traditional:40  value = obj.get_attribute()41  obj.set_attribute(value)4243With @property:44  value = obj.attribute45  obj.attribute = value4647Same control, cleaner syntax!48""")4950print("=== Multiple Properties ===")5152class Rectangle:53    def __init__(self, width, height):54        self._width = width55        self._height = height56    57    @property58    def width(self):59        return self._width60    61    @width.setter62    def width(self, value):  #?widthsetter63        if value > 0:64            self._width = value65    66    @property67    def height(self):68        return self._height69    70    @height.setter71    def height(self, value):72        if value > 0:73            self._height = value74    75    @property76    def area(self):  #?computedprop77        """Computed property - no setter."""78        return self._width * self._height79    80    @property81    def perimeter(self):82        return 2 * (self._width + self._height)8384rect = Rectangle(5, 3)  #@rect=Rectangle(4, 4), Rectangle(8, 2)85print(f"Size: {rect.width} x {rect.height}")
    outputStill: 10
    
    === Properties vs Direct Access ===
    
    Traditional:
      value = obj.get_attribute()
      obj.set_attribute(value)
    
    With @property:
      value = obj.attribute
      obj.attribute = value
    
    Same control, cleaner syntax!
    === Multiple Properties ===
  14. self._width ← 5, self._height ← 3

    52class Rectangle:53    def __init__(self⟨Rectangle B⟩, width5, height3):54        self._width→ 5 = width555        self._height→ 3 = height3
  15. rect ← ⟨Rectangle B⟩

    84rect→ ⟨Rectangle B⟩ = Rectangle(5, 3)  #@rect=Rectangle(4, 4), Rectangle(8, 2)85print(f"Size: {rect.width5} x {rect.height3}")86print(f"Area: {rect.area}")
  16. def width(self):

    pass 1 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width5
  17. def height(self):

    pass 1 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height3
  18. print(f"Size: {rect.width} x {rect.height}")

    84rect = Rectangle(5, 3)  #@rect=Rectangle(4, 4), Rectangle(8, 2)85print(f"Size: {rect.width5} x {rect.height3}")86print(f"Area: {rect.area15}")87print(f"Perimeter: {rect.perimeter}")
    outputSize: 5 x 3
  19. def area(self): #?computedprop

    pass 1 of 2
    75@property76def area(self⟨Rectangle B⟩):  #?computedprop77    """Computed property - no setter."""78    return self._width5 * self._height3
  20. print(f"Area: {rect.area}")

    85print(f"Size: {rect.width} x {rect.height}")86print(f"Area: {rect.area15}")87print(f"Perimeter: {rect.perimeter16}")
    outputArea: 15
  21. def perimeter(self):

    80@property81def perimeter(self⟨Rectangle B⟩):82    return 2 * (self._width5 + self._height3)
  22. print(f"Perimeter: {rect.perimeter}")

    86print(f"Area: {rect.area}")87print(f"Perimeter: {rect.perimeter16}")8889# Change dimensions  #?changedim90rect.width = 1091print(f"\nAfter resize:")
    outputPerimeter: 16
  23. def width(self, value): #?widthsetter

    61@width.setter62def width(self⟨Rectangle B⟩, value10):  #?widthsetter63    if value > 0:64        self._width = value
  24. self._width ← 10

    62def width(self, value):  #?widthsetter63    if value10 > 0:64        self._width→ 10 = value10
  25. rect.width ← 10

    89# Change dimensions  #?changedim90rect.width→ 10 = 1091print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height3}")93print(f"Area: {rect.area}")  # Automatically updated!
    output
    After resize:
  26. def width(self):

    pass 2 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width10
  27. def height(self):

    pass 2 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height3
  28. print(f"Size: {rect.width} x {rect.height}")

    91print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height3}")93print(f"Area: {rect.area30}")  # Automatically updated!
    outputSize: 10 x 3
  29. def area(self): #?computedprop

    pass 2 of 2
    75@property76def area(self⟨Rectangle B⟩):  #?computedprop77    """Computed property - no setter."""78    return self._width10 * self._height3
  30. print(f"Area: {rect.area}") # Automatically updated!

    92print(f"Size: {rect.width} x {rect.height}")93print(f"Area: {rect.area30}")  # Automatically updated!9495print("\n=== Property with Deleter ===")9697class User:98    def __init__(self, email):99        self._email = email100    101    @property102    def email(self):103        return self._email104    105    @email.setter106    def email(self, value):107        if "@" in value:108            self._email = value109    110    @email.deleter  #?deleter111    def email(self):112        print("Removing email...")113        self._email = None114115user = User("alice@example.com")116print(f"Email: {user.email}")
    outputArea: 30
    
    === Property with Deleter ===
  31. self._email ← alice@example.com

    97class User:98    def __init__(self⟨User C⟩, emailalice@example.com):99        self._email→ alice@example.com = emailalice@example.com
  32. user ← ⟨User C⟩

    115user→ ⟨User C⟩ = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
  33. def email(self):

    pass 1 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailalice@example.com
  34. print(f"Email: {user.email}")

    115user = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
    outputEmail: alice@example.com
  35. self._email ← None

    110@email.deleter  #?deleter111def email(self⟨User C⟩):112    print("Removing email...")113    self._email→ None = None
    outputRemoving email...
  36. print(f"After delete: {user.email}")

    118del user.email  #?usedel119print(f"After delete: {user.emailNone}")
  37. def email(self):

    pass 2 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailNone
  38. print(f"After delete: {user.email}")

    118del user.email  #?usedel119print(f"After delete: {user.emailNone}")120121print("\n=== Property Naming Patterns ===")122123class Temperature:124    def __init__(self, celsius):125        self._celsius = celsius  #?storedinternal126    127    @property128    def celsius(self):  #?samename129        return self._celsius130    131    @celsius.setter132    def celsius(self, value):133        if value >= -273.15:134            self._celsius = value135    136    @property137    def fahrenheit(self):  #?convertprop138        return self._celsius * 9/5 + 32139    140    @fahrenheit.setter  #?reverseset141    def fahrenheit(self, value):142        self._celsius = (value - 32) * 5/9143144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit}°F")
    outputAfter delete: None
    
    === Property Naming Patterns ===
  39. self._celsius ← 0

    123class Temperature:124    def __init__(self⟨Temperature D⟩, celsius0):125        self._celsius→ 0 = celsius0  #?storedinternal
  40. temp ← ⟨Temperature D⟩

    144temp→ ⟨Temperature D⟩ = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")
  41. def fahrenheit(self): #?convertprop

    pass 1 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):  #?convertprop138    return self._celsius0 * 9/5 + 32
  42. print(f"0°C = {temp.fahrenheit}°F")

    144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")146147temp.fahrenheit = 212  # Set via Fahrenheit  #?setf148print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
    output0°C = 32.0°F
  43. self._celsius ← 100.0

    140@fahrenheit.setter  #?reverseset141def fahrenheit(self⟨Temperature D⟩, value212):142    self._celsius→ 100.0 = (value212 - 32) * 5/9
  44. temp.fahrenheit ← 212.0

    147temp.fahrenheit→ 212.0 = 212  # Set via Fahrenheit  #?setf148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")149#@help propertydeco
  45. def fahrenheit(self): #?convertprop

    pass 2 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):  #?convertprop138    return self._celsius100.0 * 9/5 + 32
  46. def celsius(self): #?samename

    127@property128def celsius(self⟨Temperature D⟩):  #?samename129    return self._celsius100.0
  47. print(f"{temp.fahrenheit}°F = {temp.celsius}°C")

    147temp.fahrenheit = 212  # Set via Fahrenheit  #?setf148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")149#@help propertydeco
    output212.0°F = 100.0°C
  1. print("=== Basic @property === ")

    3print("=== Basic @property ===\n")45class Circle:6    def __init__(self, radius):7        self._radius = radius8    9    @property10    def radius(self):11        """Get the radius."""12        return self._radius13    14    @radius.setter15    def radius(self, value):16        """Set the radius with validation."""17        if value > 0:18            self._radius = value19        else:20            print(f"Invalid radius: {value}")2122circle = Circle(2)
    output=== Basic @property ===
  2. self._radius ← 2

    5class Circle:6    def __init__(self⟨Circle A⟩, radius2):7        self._radius→ 2 = radius2
  3. circle ← ⟨Circle A⟩

    22circle→ ⟨Circle A⟩ = Circle(2)2324# Access like attribute25print(f"Radius: {circle.radius2}")  # Calls getter
  4. def radius(self):

    pass 1 of 3
    9@property10def radius(self⟨Circle A⟩):11    """Get the radius."""12    return self._radius2
    All 3 passes — pass 1 is the card above
    passself._radius
    12
    210
    310
  5. print(f"Radius: {circle.radius}") # Calls getter

    24# Access like attribute25print(f"Radius: {circle.radius2}")  # Calls getter2627# Assign like attribute28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius}")
    outputRadius: 2
  6. def radius(self, value):

    pass 1 of 2
    14@radius.setter15def radius(self⟨Circle A⟩, value10):16    """Set the radius with validation."""17    if value > 0:
  7. self._radius ← 10

    16"""Set the radius with validation."""17if value10 > 0:18    self._radius→ 10 = value1019else:
  8. circle.radius ← 10

    27# Assign like attribute28circle.radius→ 10 = 10  # Calls setter29print(f"New radius: {circle.radius10}")
  9. print(f"New radius: {circle.radius}")

    28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius10}")3031# Validation still works32circle.radius = -5  # Rejected33print(f"Still: {circle.radius}")
    outputNew radius: 10
  10. def radius(self, value):

    pass 2 of 2
    14@radius.setter15def radius(self⟨Circle A⟩, value-5):16    """Set the radius with validation."""17    if value > 0:
  11. else:

    17if value > 0:18    self._radius = value19else:20    print(f"Invalid radius: {value-5}")
    outputInvalid radius: -5
  12. circle.radius ← 10

    31# Validation still works32circle.radius→ 10 = -5  # Rejected33print(f"Still: {circle.radius10}")
  13. print(f"Still: {circle.radius}")

    32circle.radius = -5  # Rejected33print(f"Still: {circle.radius10}")3435print("\n=== Properties vs Direct Access ===")3637# Compare syntax:38print("""39Traditional:40  value = obj.get_attribute()41  obj.set_attribute(value)4243With @property:44  value = obj.attribute45  obj.attribute = value4647Same control, cleaner syntax!48""")4950print("=== Multiple Properties ===")5152class Rectangle:53    def __init__(self, width, height):54        self._width = width55        self._height = height56    57    @property58    def width(self):59        return self._width60    61    @width.setter62    def width(self, value):63        if value > 0:64            self._width = value65    66    @property67    def height(self):68        return self._height69    70    @height.setter71    def height(self, value):72        if value > 0:73            self._height = value74    75    @property76    def area(self):77        """Computed property - no setter."""78        return self._width * self._height79    80    @property81    def perimeter(self):82        return 2 * (self._width + self._height)8384rect = Rectangle(5, 3)85print(f"Size: {rect.width} x {rect.height}")
    outputStill: 10
    
    === Properties vs Direct Access ===
    
    Traditional:
      value = obj.get_attribute()
      obj.set_attribute(value)
    
    With @property:
      value = obj.attribute
      obj.attribute = value
    
    Same control, cleaner syntax!
    === Multiple Properties ===
  14. self._width ← 5, self._height ← 3

    52class Rectangle:53    def __init__(self⟨Rectangle B⟩, width5, height3):54        self._width→ 5 = width555        self._height→ 3 = height3
  15. rect ← ⟨Rectangle B⟩

    84rect→ ⟨Rectangle B⟩ = Rectangle(5, 3)85print(f"Size: {rect.width5} x {rect.height3}")86print(f"Area: {rect.area}")
  16. def width(self):

    pass 1 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width5
  17. def height(self):

    pass 1 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height3
  18. print(f"Size: {rect.width} x {rect.height}")

    84rect = Rectangle(5, 3)85print(f"Size: {rect.width5} x {rect.height3}")86print(f"Area: {rect.area15}")87print(f"Perimeter: {rect.perimeter}")
    outputSize: 5 x 3
  19. def area(self):

    pass 1 of 2
    75@property76def area(self⟨Rectangle B⟩):77    """Computed property - no setter."""78    return self._width5 * self._height3
  20. print(f"Area: {rect.area}")

    85print(f"Size: {rect.width} x {rect.height}")86print(f"Area: {rect.area15}")87print(f"Perimeter: {rect.perimeter16}")
    outputArea: 15
  21. def perimeter(self):

    80@property81def perimeter(self⟨Rectangle B⟩):82    return 2 * (self._width5 + self._height3)
  22. print(f"Perimeter: {rect.perimeter}")

    86print(f"Area: {rect.area}")87print(f"Perimeter: {rect.perimeter16}")8889# Change dimensions90rect.width = 1091print(f"\nAfter resize:")
    outputPerimeter: 16
  23. def width(self, value):

    61@width.setter62def width(self⟨Rectangle B⟩, value10):63    if value > 0:64        self._width = value
  24. self._width ← 10

    62def width(self, value):63    if value10 > 0:64        self._width→ 10 = value10
  25. rect.width ← 10

    89# Change dimensions90rect.width→ 10 = 1091print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height3}")93print(f"Area: {rect.area}")  # Automatically updated!
    output
    After resize:
  26. def width(self):

    pass 2 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width10
  27. def height(self):

    pass 2 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height3
  28. print(f"Size: {rect.width} x {rect.height}")

    91print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height3}")93print(f"Area: {rect.area30}")  # Automatically updated!
    outputSize: 10 x 3
  29. def area(self):

    pass 2 of 2
    75@property76def area(self⟨Rectangle B⟩):77    """Computed property - no setter."""78    return self._width10 * self._height3
  30. print(f"Area: {rect.area}") # Automatically updated!

    92print(f"Size: {rect.width} x {rect.height}")93print(f"Area: {rect.area30}")  # Automatically updated!9495print("\n=== Property with Deleter ===")9697class User:98    def __init__(self, email):99        self._email = email100    101    @property102    def email(self):103        return self._email104    105    @email.setter106    def email(self, value):107        if "@" in value:108            self._email = value109    110    @email.deleter111    def email(self):112        print("Removing email...")113        self._email = None114115user = User("alice@example.com")116print(f"Email: {user.email}")
    outputArea: 30
    
    === Property with Deleter ===
  31. self._email ← alice@example.com

    97class User:98    def __init__(self⟨User C⟩, emailalice@example.com):99        self._email→ alice@example.com = emailalice@example.com
  32. user ← ⟨User C⟩

    115user→ ⟨User C⟩ = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
  33. def email(self):

    pass 1 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailalice@example.com
  34. print(f"Email: {user.email}")

    115user = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
    outputEmail: alice@example.com
  35. self._email ← None

    110@email.deleter111def email(self⟨User C⟩):112    print("Removing email...")113    self._email→ None = None
    outputRemoving email...
  36. print(f"After delete: {user.email}")

    118del user.email119print(f"After delete: {user.emailNone}")
  37. def email(self):

    pass 2 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailNone
  38. print(f"After delete: {user.email}")

    118del user.email119print(f"After delete: {user.emailNone}")120121print("\n=== Property Naming Patterns ===")122123class Temperature:124    def __init__(self, celsius):125        self._celsius = celsius126    127    @property128    def celsius(self):129        return self._celsius130    131    @celsius.setter132    def celsius(self, value):133        if value >= -273.15:134            self._celsius = value135    136    @property137    def fahrenheit(self):138        return self._celsius * 9/5 + 32139    140    @fahrenheit.setter141    def fahrenheit(self, value):142        self._celsius = (value - 32) * 5/9143144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit}°F")
    outputAfter delete: None
    
    === Property Naming Patterns ===
  39. self._celsius ← 0

    123class Temperature:124    def __init__(self⟨Temperature D⟩, celsius0):125        self._celsius→ 0 = celsius0
  40. temp ← ⟨Temperature D⟩

    144temp→ ⟨Temperature D⟩ = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")
  41. def fahrenheit(self):

    pass 1 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):138    return self._celsius0 * 9/5 + 32
  42. print(f"0°C = {temp.fahrenheit}°F")

    144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")146147temp.fahrenheit = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
    output0°C = 32.0°F
  43. self._celsius ← 100.0

    140@fahrenheit.setter141def fahrenheit(self⟨Temperature D⟩, value212):142    self._celsius→ 100.0 = (value212 - 32) * 5/9
  44. temp.fahrenheit ← 212.0

    147temp.fahrenheit→ 212.0 = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")
  45. def fahrenheit(self):

    pass 2 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):138    return self._celsius100.0 * 9/5 + 32
  46. def celsius(self):

    127@property128def celsius(self⟨Temperature D⟩):129    return self._celsius100.0
  47. print(f"{temp.fahrenheit}°F = {temp.celsius}°C")

    147temp.fahrenheit = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")
    output212.0°F = 100.0°C
  1. print("=== Basic @property === ")

    3print("=== Basic @property ===\n")45class Circle:6    def __init__(self, radius):7        self._radius = radius8    9    @property10    def radius(self):11        """Get the radius."""12        return self._radius13    14    @radius.setter15    def radius(self, value):16        """Set the radius with validation."""17        if value > 0:18            self._radius = value19        else:20            print(f"Invalid radius: {value}")2122circle = Circle(12)
    output=== Basic @property ===
  2. self._radius ← 12

    5class Circle:6    def __init__(self⟨Circle A⟩, radius12):7        self._radius→ 12 = radius12
  3. circle ← ⟨Circle A⟩

    22circle→ ⟨Circle A⟩ = Circle(12)2324# Access like attribute25print(f"Radius: {circle.radius12}")  # Calls getter
  4. def radius(self):

    pass 1 of 3
    9@property10def radius(self⟨Circle A⟩):11    """Get the radius."""12    return self._radius12
    All 3 passes — pass 1 is the card above
    passself._radius
    112
    210
    310
  5. print(f"Radius: {circle.radius}") # Calls getter

    24# Access like attribute25print(f"Radius: {circle.radius12}")  # Calls getter2627# Assign like attribute28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius}")
    outputRadius: 12
  6. def radius(self, value):

    pass 1 of 2
    14@radius.setter15def radius(self⟨Circle A⟩, value10):16    """Set the radius with validation."""17    if value > 0:
  7. self._radius ← 10

    16"""Set the radius with validation."""17if value10 > 0:18    self._radius→ 10 = value1019else:
  8. circle.radius ← 10

    27# Assign like attribute28circle.radius→ 10 = 10  # Calls setter29print(f"New radius: {circle.radius10}")
  9. print(f"New radius: {circle.radius}")

    28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius10}")3031# Validation still works32circle.radius = -5  # Rejected33print(f"Still: {circle.radius}")
    outputNew radius: 10
  10. def radius(self, value):

    pass 2 of 2
    14@radius.setter15def radius(self⟨Circle A⟩, value-5):16    """Set the radius with validation."""17    if value > 0:
  11. else:

    17if value > 0:18    self._radius = value19else:20    print(f"Invalid radius: {value-5}")
    outputInvalid radius: -5
  12. circle.radius ← 10

    31# Validation still works32circle.radius→ 10 = -5  # Rejected33print(f"Still: {circle.radius10}")
  13. print(f"Still: {circle.radius}")

    32circle.radius = -5  # Rejected33print(f"Still: {circle.radius10}")3435print("\n=== Properties vs Direct Access ===")3637# Compare syntax:38print("""39Traditional:40  value = obj.get_attribute()41  obj.set_attribute(value)4243With @property:44  value = obj.attribute45  obj.attribute = value4647Same control, cleaner syntax!48""")4950print("=== Multiple Properties ===")5152class Rectangle:53    def __init__(self, width, height):54        self._width = width55        self._height = height56    57    @property58    def width(self):59        return self._width60    61    @width.setter62    def width(self, value):63        if value > 0:64            self._width = value65    66    @property67    def height(self):68        return self._height69    70    @height.setter71    def height(self, value):72        if value > 0:73            self._height = value74    75    @property76    def area(self):77        """Computed property - no setter."""78        return self._width * self._height79    80    @property81    def perimeter(self):82        return 2 * (self._width + self._height)8384rect = Rectangle(5, 3)85print(f"Size: {rect.width} x {rect.height}")
    outputStill: 10
    
    === Properties vs Direct Access ===
    
    Traditional:
      value = obj.get_attribute()
      obj.set_attribute(value)
    
    With @property:
      value = obj.attribute
      obj.attribute = value
    
    Same control, cleaner syntax!
    === Multiple Properties ===
  14. self._width ← 5, self._height ← 3

    52class Rectangle:53    def __init__(self⟨Rectangle B⟩, width5, height3):54        self._width→ 5 = width555        self._height→ 3 = height3
  15. rect ← ⟨Rectangle B⟩

    84rect→ ⟨Rectangle B⟩ = Rectangle(5, 3)85print(f"Size: {rect.width5} x {rect.height3}")86print(f"Area: {rect.area}")
  16. def width(self):

    pass 1 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width5
  17. def height(self):

    pass 1 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height3
  18. print(f"Size: {rect.width} x {rect.height}")

    84rect = Rectangle(5, 3)85print(f"Size: {rect.width5} x {rect.height3}")86print(f"Area: {rect.area15}")87print(f"Perimeter: {rect.perimeter}")
    outputSize: 5 x 3
  19. def area(self):

    pass 1 of 2
    75@property76def area(self⟨Rectangle B⟩):77    """Computed property - no setter."""78    return self._width5 * self._height3
  20. print(f"Area: {rect.area}")

    85print(f"Size: {rect.width} x {rect.height}")86print(f"Area: {rect.area15}")87print(f"Perimeter: {rect.perimeter16}")
    outputArea: 15
  21. def perimeter(self):

    80@property81def perimeter(self⟨Rectangle B⟩):82    return 2 * (self._width5 + self._height3)
  22. print(f"Perimeter: {rect.perimeter}")

    86print(f"Area: {rect.area}")87print(f"Perimeter: {rect.perimeter16}")8889# Change dimensions90rect.width = 1091print(f"\nAfter resize:")
    outputPerimeter: 16
  23. def width(self, value):

    61@width.setter62def width(self⟨Rectangle B⟩, value10):63    if value > 0:64        self._width = value
  24. self._width ← 10

    62def width(self, value):63    if value10 > 0:64        self._width→ 10 = value10
  25. rect.width ← 10

    89# Change dimensions90rect.width→ 10 = 1091print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height3}")93print(f"Area: {rect.area}")  # Automatically updated!
    output
    After resize:
  26. def width(self):

    pass 2 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width10
  27. def height(self):

    pass 2 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height3
  28. print(f"Size: {rect.width} x {rect.height}")

    91print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height3}")93print(f"Area: {rect.area30}")  # Automatically updated!
    outputSize: 10 x 3
  29. def area(self):

    pass 2 of 2
    75@property76def area(self⟨Rectangle B⟩):77    """Computed property - no setter."""78    return self._width10 * self._height3
  30. print(f"Area: {rect.area}") # Automatically updated!

    92print(f"Size: {rect.width} x {rect.height}")93print(f"Area: {rect.area30}")  # Automatically updated!9495print("\n=== Property with Deleter ===")9697class User:98    def __init__(self, email):99        self._email = email100    101    @property102    def email(self):103        return self._email104    105    @email.setter106    def email(self, value):107        if "@" in value:108            self._email = value109    110    @email.deleter111    def email(self):112        print("Removing email...")113        self._email = None114115user = User("alice@example.com")116print(f"Email: {user.email}")
    outputArea: 30
    
    === Property with Deleter ===
  31. self._email ← alice@example.com

    97class User:98    def __init__(self⟨User C⟩, emailalice@example.com):99        self._email→ alice@example.com = emailalice@example.com
  32. user ← ⟨User C⟩

    115user→ ⟨User C⟩ = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
  33. def email(self):

    pass 1 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailalice@example.com
  34. print(f"Email: {user.email}")

    115user = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
    outputEmail: alice@example.com
  35. self._email ← None

    110@email.deleter111def email(self⟨User C⟩):112    print("Removing email...")113    self._email→ None = None
    outputRemoving email...
  36. print(f"After delete: {user.email}")

    118del user.email119print(f"After delete: {user.emailNone}")
  37. def email(self):

    pass 2 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailNone
  38. print(f"After delete: {user.email}")

    118del user.email119print(f"After delete: {user.emailNone}")120121print("\n=== Property Naming Patterns ===")122123class Temperature:124    def __init__(self, celsius):125        self._celsius = celsius126    127    @property128    def celsius(self):129        return self._celsius130    131    @celsius.setter132    def celsius(self, value):133        if value >= -273.15:134            self._celsius = value135    136    @property137    def fahrenheit(self):138        return self._celsius * 9/5 + 32139    140    @fahrenheit.setter141    def fahrenheit(self, value):142        self._celsius = (value - 32) * 5/9143144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit}°F")
    outputAfter delete: None
    
    === Property Naming Patterns ===
  39. self._celsius ← 0

    123class Temperature:124    def __init__(self⟨Temperature D⟩, celsius0):125        self._celsius→ 0 = celsius0
  40. temp ← ⟨Temperature D⟩

    144temp→ ⟨Temperature D⟩ = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")
  41. def fahrenheit(self):

    pass 1 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):138    return self._celsius0 * 9/5 + 32
  42. print(f"0°C = {temp.fahrenheit}°F")

    144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")146147temp.fahrenheit = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
    output0°C = 32.0°F
  43. self._celsius ← 100.0

    140@fahrenheit.setter141def fahrenheit(self⟨Temperature D⟩, value212):142    self._celsius→ 100.0 = (value212 - 32) * 5/9
  44. temp.fahrenheit ← 212.0

    147temp.fahrenheit→ 212.0 = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")
  45. def fahrenheit(self):

    pass 2 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):138    return self._celsius100.0 * 9/5 + 32
  46. def celsius(self):

    127@property128def celsius(self⟨Temperature D⟩):129    return self._celsius100.0
  47. print(f"{temp.fahrenheit}°F = {temp.celsius}°C")

    147temp.fahrenheit = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")
    output212.0°F = 100.0°C
  1. print("=== Basic @property === ")

    3print("=== Basic @property ===\n")45class Circle:6    def __init__(self, radius):7        self._radius = radius8    9    @property10    def radius(self):11        """Get the radius."""12        return self._radius13    14    @radius.setter15    def radius(self, value):16        """Set the radius with validation."""17        if value > 0:18            self._radius = value19        else:20            print(f"Invalid radius: {value}")2122circle = Circle(5)
    output=== Basic @property ===
  2. self._radius ← 5

    5class Circle:6    def __init__(self⟨Circle A⟩, radius5):7        self._radius→ 5 = radius5
  3. circle ← ⟨Circle A⟩

    22circle→ ⟨Circle A⟩ = Circle(5)2324# Access like attribute25print(f"Radius: {circle.radius5}")  # Calls getter
  4. def radius(self):

    pass 1 of 3
    9@property10def radius(self⟨Circle A⟩):11    """Get the radius."""12    return self._radius5
    All 3 passes — pass 1 is the card above
    passself._radius
    15
    210
    310
  5. print(f"Radius: {circle.radius}") # Calls getter

    24# Access like attribute25print(f"Radius: {circle.radius5}")  # Calls getter2627# Assign like attribute28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius}")
    outputRadius: 5
  6. def radius(self, value):

    pass 1 of 2
    14@radius.setter15def radius(self⟨Circle A⟩, value10):16    """Set the radius with validation."""17    if value > 0:
  7. self._radius ← 10

    16"""Set the radius with validation."""17if value10 > 0:18    self._radius→ 10 = value1019else:
  8. circle.radius ← 10

    27# Assign like attribute28circle.radius→ 10 = 10  # Calls setter29print(f"New radius: {circle.radius10}")
  9. print(f"New radius: {circle.radius}")

    28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius10}")3031# Validation still works32circle.radius = -5  # Rejected33print(f"Still: {circle.radius}")
    outputNew radius: 10
  10. def radius(self, value):

    pass 2 of 2
    14@radius.setter15def radius(self⟨Circle A⟩, value-5):16    """Set the radius with validation."""17    if value > 0:
  11. else:

    17if value > 0:18    self._radius = value19else:20    print(f"Invalid radius: {value-5}")
    outputInvalid radius: -5
  12. circle.radius ← 10

    31# Validation still works32circle.radius→ 10 = -5  # Rejected33print(f"Still: {circle.radius10}")
  13. print(f"Still: {circle.radius}")

    32circle.radius = -5  # Rejected33print(f"Still: {circle.radius10}")3435print("\n=== Properties vs Direct Access ===")3637# Compare syntax:38print("""39Traditional:40  value = obj.get_attribute()41  obj.set_attribute(value)4243With @property:44  value = obj.attribute45  obj.attribute = value4647Same control, cleaner syntax!48""")4950print("=== Multiple Properties ===")5152class Rectangle:53    def __init__(self, width, height):54        self._width = width55        self._height = height56    57    @property58    def width(self):59        return self._width60    61    @width.setter62    def width(self, value):63        if value > 0:64            self._width = value65    66    @property67    def height(self):68        return self._height69    70    @height.setter71    def height(self, value):72        if value > 0:73            self._height = value74    75    @property76    def area(self):77        """Computed property - no setter."""78        return self._width * self._height79    80    @property81    def perimeter(self):82        return 2 * (self._width + self._height)8384rect = Rectangle(4, 4)85print(f"Size: {rect.width} x {rect.height}")
    outputStill: 10
    
    === Properties vs Direct Access ===
    
    Traditional:
      value = obj.get_attribute()
      obj.set_attribute(value)
    
    With @property:
      value = obj.attribute
      obj.attribute = value
    
    Same control, cleaner syntax!
    === Multiple Properties ===
  14. self._width ← 4, self._height ← 4

    52class Rectangle:53    def __init__(self⟨Rectangle B⟩, width4, height4):54        self._width→ 4 = width455        self._height→ 4 = height4
  15. rect ← ⟨Rectangle B⟩

    84rect→ ⟨Rectangle B⟩ = Rectangle(4, 4)85print(f"Size: {rect.width4} x {rect.height4}")86print(f"Area: {rect.area}")
  16. def width(self):

    pass 1 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width4
  17. def height(self):

    pass 1 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height4
  18. print(f"Size: {rect.width} x {rect.height}")

    84rect = Rectangle(4, 4)85print(f"Size: {rect.width4} x {rect.height4}")86print(f"Area: {rect.area16}")87print(f"Perimeter: {rect.perimeter}")
    outputSize: 4 x 4
  19. def area(self):

    pass 1 of 2
    75@property76def area(self⟨Rectangle B⟩):77    """Computed property - no setter."""78    return self._width4 * self._height4
  20. print(f"Area: {rect.area}")

    85print(f"Size: {rect.width} x {rect.height}")86print(f"Area: {rect.area16}")87print(f"Perimeter: {rect.perimeter16}")
    outputArea: 16
  21. def perimeter(self):

    80@property81def perimeter(self⟨Rectangle B⟩):82    return 2 * (self._width4 + self._height4)
  22. print(f"Perimeter: {rect.perimeter}")

    86print(f"Area: {rect.area}")87print(f"Perimeter: {rect.perimeter16}")8889# Change dimensions90rect.width = 1091print(f"\nAfter resize:")
    outputPerimeter: 16
  23. def width(self, value):

    61@width.setter62def width(self⟨Rectangle B⟩, value10):63    if value > 0:64        self._width = value
  24. self._width ← 10

    62def width(self, value):63    if value10 > 0:64        self._width→ 10 = value10
  25. rect.width ← 10

    89# Change dimensions90rect.width→ 10 = 1091print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height4}")93print(f"Area: {rect.area}")  # Automatically updated!
    output
    After resize:
  26. def width(self):

    pass 2 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width10
  27. def height(self):

    pass 2 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height4
  28. print(f"Size: {rect.width} x {rect.height}")

    91print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height4}")93print(f"Area: {rect.area40}")  # Automatically updated!
    outputSize: 10 x 4
  29. def area(self):

    pass 2 of 2
    75@property76def area(self⟨Rectangle B⟩):77    """Computed property - no setter."""78    return self._width10 * self._height4
  30. print(f"Area: {rect.area}") # Automatically updated!

    92print(f"Size: {rect.width} x {rect.height}")93print(f"Area: {rect.area40}")  # Automatically updated!9495print("\n=== Property with Deleter ===")9697class User:98    def __init__(self, email):99        self._email = email100    101    @property102    def email(self):103        return self._email104    105    @email.setter106    def email(self, value):107        if "@" in value:108            self._email = value109    110    @email.deleter111    def email(self):112        print("Removing email...")113        self._email = None114115user = User("alice@example.com")116print(f"Email: {user.email}")
    outputArea: 40
    
    === Property with Deleter ===
  31. self._email ← alice@example.com

    97class User:98    def __init__(self⟨User C⟩, emailalice@example.com):99        self._email→ alice@example.com = emailalice@example.com
  32. user ← ⟨User C⟩

    115user→ ⟨User C⟩ = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
  33. def email(self):

    pass 1 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailalice@example.com
  34. print(f"Email: {user.email}")

    115user = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
    outputEmail: alice@example.com
  35. self._email ← None

    110@email.deleter111def email(self⟨User C⟩):112    print("Removing email...")113    self._email→ None = None
    outputRemoving email...
  36. print(f"After delete: {user.email}")

    118del user.email119print(f"After delete: {user.emailNone}")
  37. def email(self):

    pass 2 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailNone
  38. print(f"After delete: {user.email}")

    118del user.email119print(f"After delete: {user.emailNone}")120121print("\n=== Property Naming Patterns ===")122123class Temperature:124    def __init__(self, celsius):125        self._celsius = celsius126    127    @property128    def celsius(self):129        return self._celsius130    131    @celsius.setter132    def celsius(self, value):133        if value >= -273.15:134            self._celsius = value135    136    @property137    def fahrenheit(self):138        return self._celsius * 9/5 + 32139    140    @fahrenheit.setter141    def fahrenheit(self, value):142        self._celsius = (value - 32) * 5/9143144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit}°F")
    outputAfter delete: None
    
    === Property Naming Patterns ===
  39. self._celsius ← 0

    123class Temperature:124    def __init__(self⟨Temperature D⟩, celsius0):125        self._celsius→ 0 = celsius0
  40. temp ← ⟨Temperature D⟩

    144temp→ ⟨Temperature D⟩ = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")
  41. def fahrenheit(self):

    pass 1 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):138    return self._celsius0 * 9/5 + 32
  42. print(f"0°C = {temp.fahrenheit}°F")

    144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")146147temp.fahrenheit = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
    output0°C = 32.0°F
  43. self._celsius ← 100.0

    140@fahrenheit.setter141def fahrenheit(self⟨Temperature D⟩, value212):142    self._celsius→ 100.0 = (value212 - 32) * 5/9
  44. temp.fahrenheit ← 212.0

    147temp.fahrenheit→ 212.0 = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")
  45. def fahrenheit(self):

    pass 2 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):138    return self._celsius100.0 * 9/5 + 32
  46. def celsius(self):

    127@property128def celsius(self⟨Temperature D⟩):129    return self._celsius100.0
  47. print(f"{temp.fahrenheit}°F = {temp.celsius}°C")

    147temp.fahrenheit = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")
    output212.0°F = 100.0°C
  1. print("=== Basic @property === ")

    3print("=== Basic @property ===\n")45class Circle:6    def __init__(self, radius):7        self._radius = radius8    9    @property10    def radius(self):11        """Get the radius."""12        return self._radius13    14    @radius.setter15    def radius(self, value):16        """Set the radius with validation."""17        if value > 0:18            self._radius = value19        else:20            print(f"Invalid radius: {value}")2122circle = Circle(5)
    output=== Basic @property ===
  2. self._radius ← 5

    5class Circle:6    def __init__(self⟨Circle A⟩, radius5):7        self._radius→ 5 = radius5
  3. circle ← ⟨Circle A⟩

    22circle→ ⟨Circle A⟩ = Circle(5)2324# Access like attribute25print(f"Radius: {circle.radius5}")  # Calls getter
  4. def radius(self):

    pass 1 of 3
    9@property10def radius(self⟨Circle A⟩):11    """Get the radius."""12    return self._radius5
    All 3 passes — pass 1 is the card above
    passself._radius
    15
    210
    310
  5. print(f"Radius: {circle.radius}") # Calls getter

    24# Access like attribute25print(f"Radius: {circle.radius5}")  # Calls getter2627# Assign like attribute28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius}")
    outputRadius: 5
  6. def radius(self, value):

    pass 1 of 2
    14@radius.setter15def radius(self⟨Circle A⟩, value10):16    """Set the radius with validation."""17    if value > 0:
  7. self._radius ← 10

    16"""Set the radius with validation."""17if value10 > 0:18    self._radius→ 10 = value1019else:
  8. circle.radius ← 10

    27# Assign like attribute28circle.radius→ 10 = 10  # Calls setter29print(f"New radius: {circle.radius10}")
  9. print(f"New radius: {circle.radius}")

    28circle.radius = 10  # Calls setter29print(f"New radius: {circle.radius10}")3031# Validation still works32circle.radius = -5  # Rejected33print(f"Still: {circle.radius}")
    outputNew radius: 10
  10. def radius(self, value):

    pass 2 of 2
    14@radius.setter15def radius(self⟨Circle A⟩, value-5):16    """Set the radius with validation."""17    if value > 0:
  11. else:

    17if value > 0:18    self._radius = value19else:20    print(f"Invalid radius: {value-5}")
    outputInvalid radius: -5
  12. circle.radius ← 10

    31# Validation still works32circle.radius→ 10 = -5  # Rejected33print(f"Still: {circle.radius10}")
  13. print(f"Still: {circle.radius}")

    32circle.radius = -5  # Rejected33print(f"Still: {circle.radius10}")3435print("\n=== Properties vs Direct Access ===")3637# Compare syntax:38print("""39Traditional:40  value = obj.get_attribute()41  obj.set_attribute(value)4243With @property:44  value = obj.attribute45  obj.attribute = value4647Same control, cleaner syntax!48""")4950print("=== Multiple Properties ===")5152class Rectangle:53    def __init__(self, width, height):54        self._width = width55        self._height = height56    57    @property58    def width(self):59        return self._width60    61    @width.setter62    def width(self, value):63        if value > 0:64            self._width = value65    66    @property67    def height(self):68        return self._height69    70    @height.setter71    def height(self, value):72        if value > 0:73            self._height = value74    75    @property76    def area(self):77        """Computed property - no setter."""78        return self._width * self._height79    80    @property81    def perimeter(self):82        return 2 * (self._width + self._height)8384rect = Rectangle(8, 2)85print(f"Size: {rect.width} x {rect.height}")
    outputStill: 10
    
    === Properties vs Direct Access ===
    
    Traditional:
      value = obj.get_attribute()
      obj.set_attribute(value)
    
    With @property:
      value = obj.attribute
      obj.attribute = value
    
    Same control, cleaner syntax!
    === Multiple Properties ===
  14. self._width ← 8, self._height ← 2

    52class Rectangle:53    def __init__(self⟨Rectangle B⟩, width8, height2):54        self._width→ 8 = width855        self._height→ 2 = height2
  15. rect ← ⟨Rectangle B⟩

    84rect→ ⟨Rectangle B⟩ = Rectangle(8, 2)85print(f"Size: {rect.width8} x {rect.height2}")86print(f"Area: {rect.area}")
  16. def width(self):

    pass 1 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width8
  17. def height(self):

    pass 1 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height2
  18. print(f"Size: {rect.width} x {rect.height}")

    84rect = Rectangle(8, 2)85print(f"Size: {rect.width8} x {rect.height2}")86print(f"Area: {rect.area16}")87print(f"Perimeter: {rect.perimeter}")
    outputSize: 8 x 2
  19. def area(self):

    pass 1 of 2
    75@property76def area(self⟨Rectangle B⟩):77    """Computed property - no setter."""78    return self._width8 * self._height2
  20. print(f"Area: {rect.area}")

    85print(f"Size: {rect.width} x {rect.height}")86print(f"Area: {rect.area16}")87print(f"Perimeter: {rect.perimeter20}")
    outputArea: 16
  21. def perimeter(self):

    80@property81def perimeter(self⟨Rectangle B⟩):82    return 2 * (self._width8 + self._height2)
  22. print(f"Perimeter: {rect.perimeter}")

    86print(f"Area: {rect.area}")87print(f"Perimeter: {rect.perimeter20}")8889# Change dimensions90rect.width = 1091print(f"\nAfter resize:")
    outputPerimeter: 20
  23. def width(self, value):

    61@width.setter62def width(self⟨Rectangle B⟩, value10):63    if value > 0:64        self._width = value
  24. self._width ← 10

    62def width(self, value):63    if value10 > 0:64        self._width→ 10 = value10
  25. rect.width ← 10

    89# Change dimensions90rect.width→ 10 = 1091print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height2}")93print(f"Area: {rect.area}")  # Automatically updated!
    output
    After resize:
  26. def width(self):

    pass 2 of 2
    57@property58def width(self⟨Rectangle B⟩):59    return self._width10
  27. def height(self):

    pass 2 of 2
    66@property67def height(self⟨Rectangle B⟩):68    return self._height2
  28. print(f"Size: {rect.width} x {rect.height}")

    91print(f"\nAfter resize:")92print(f"Size: {rect.width10} x {rect.height2}")93print(f"Area: {rect.area20}")  # Automatically updated!
    outputSize: 10 x 2
  29. def area(self):

    pass 2 of 2
    75@property76def area(self⟨Rectangle B⟩):77    """Computed property - no setter."""78    return self._width10 * self._height2
  30. print(f"Area: {rect.area}") # Automatically updated!

    92print(f"Size: {rect.width} x {rect.height}")93print(f"Area: {rect.area20}")  # Automatically updated!9495print("\n=== Property with Deleter ===")9697class User:98    def __init__(self, email):99        self._email = email100    101    @property102    def email(self):103        return self._email104    105    @email.setter106    def email(self, value):107        if "@" in value:108            self._email = value109    110    @email.deleter111    def email(self):112        print("Removing email...")113        self._email = None114115user = User("alice@example.com")116print(f"Email: {user.email}")
    outputArea: 20
    
    === Property with Deleter ===
  31. self._email ← alice@example.com

    97class User:98    def __init__(self⟨User C⟩, emailalice@example.com):99        self._email→ alice@example.com = emailalice@example.com
  32. user ← ⟨User C⟩

    115user→ ⟨User C⟩ = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
  33. def email(self):

    pass 1 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailalice@example.com
  34. print(f"Email: {user.email}")

    115user = User("alice@example.com")116print(f"Email: {user.emailalice@example.com}")
    outputEmail: alice@example.com
  35. self._email ← None

    110@email.deleter111def email(self⟨User C⟩):112    print("Removing email...")113    self._email→ None = None
    outputRemoving email...
  36. print(f"After delete: {user.email}")

    118del user.email119print(f"After delete: {user.emailNone}")
  37. def email(self):

    pass 2 of 2
    101@property102def email(self⟨User C⟩):103    return self._emailNone
  38. print(f"After delete: {user.email}")

    118del user.email119print(f"After delete: {user.emailNone}")120121print("\n=== Property Naming Patterns ===")122123class Temperature:124    def __init__(self, celsius):125        self._celsius = celsius126    127    @property128    def celsius(self):129        return self._celsius130    131    @celsius.setter132    def celsius(self, value):133        if value >= -273.15:134            self._celsius = value135    136    @property137    def fahrenheit(self):138        return self._celsius * 9/5 + 32139    140    @fahrenheit.setter141    def fahrenheit(self, value):142        self._celsius = (value - 32) * 5/9143144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit}°F")
    outputAfter delete: None
    
    === Property Naming Patterns ===
  39. self._celsius ← 0

    123class Temperature:124    def __init__(self⟨Temperature D⟩, celsius0):125        self._celsius→ 0 = celsius0
  40. temp ← ⟨Temperature D⟩

    144temp→ ⟨Temperature D⟩ = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")
  41. def fahrenheit(self):

    pass 1 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):138    return self._celsius0 * 9/5 + 32
  42. print(f"0°C = {temp.fahrenheit}°F")

    144temp = Temperature(0)145print(f"0°C = {temp.fahrenheit32.0}°F")146147temp.fahrenheit = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
    output0°C = 32.0°F
  43. self._celsius ← 100.0

    140@fahrenheit.setter141def fahrenheit(self⟨Temperature D⟩, value212):142    self._celsius→ 100.0 = (value212 - 32) * 5/9
  44. temp.fahrenheit ← 212.0

    147temp.fahrenheit→ 212.0 = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")
  45. def fahrenheit(self):

    pass 2 of 2
    136@property137def fahrenheit(self⟨Temperature D⟩):138    return self._celsius100.0 * 9/5 + 32
  46. def celsius(self):

    127@property128def celsius(self⟨Temperature D⟩):129    return self._celsius100.0
  47. print(f"{temp.fahrenheit}°F = {temp.celsius}°C")

    147temp.fahrenheit = 212  # Set via Fahrenheit148print(f"{temp.fahrenheit212.0}°F = {temp.celsius100.0}°C")
    output212.0°F = 100.0°C

@property creates a getter. @name.setter creates a setter. Clean syntax.

property Attribute that runs code on access: `@property` decorator.

Read-only properties

Attributes that can't be changed after creation.

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

print("=== Read-Only Properties ===\n")

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

    @property
    def x(self):
        return self._x

    @property
    def y(self):
        return self._y

    # No setters! Can't modify x or y

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

# Reading works
print(f"X coordinate: {point.x}")

# Writing fails
try:
    point.x = 10  # AttributeError!
except AttributeError as e:
    print(f"Error: can't set x - {e}")

print("\n=== Computed Read-Only Properties ===")

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

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

    @radius.setter
    def radius(self, value):
        if value > 0:
            self._radius = value

    @property
    def diameter(self):
        return self._radius * 2

    @property
    def circumference(self):
        import math
        return 2 * math.pi * self._radius

    @property
    def area(self):
        import math
        return math.pi * self._radius ** 2

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

# Change radius - computed properties update
circle.radius = 10
print(f"\nAfter radius = 10:")
print(f"Diameter: {circle.diameter}")
print(f"Area: {circle.area:.2f}")

print("\n=== ID and Timestamp Properties ===")


class Document:
    _next_id = 1

    def __init__(self, title, content):
        self._id = Document._next_id
        Document._next_id += 1
        self._created = 1736937000.0
        self._title = title
        self._content = content

    @property
    def id(self):
        return self._id

    @property
    def created(self):
        return self._created

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, value):
        self._title = value

    @property
    def content(self):
        return self._content

    @content.setter
    def content(self, value):
        self._content = value

doc = Document("Report", "Initial content")
print(f"ID: {doc.id}")
print(f"Created: {doc.created}")
print(f"Title: {doc.title}")

# Can modify title and content
doc.title = "Updated Report"
print(f"New title: {doc.title}")

# Can't modify id or created
try:
    doc.id = 999
except AttributeError:
    print("Can't change ID!")

print("\n=== Derived Properties ===")

class Person:
    def __init__(self, first_name, last_name, birth_year):
        self._first = first_name
        self._last = last_name
        self._birth_year = birth_year

    @property
    def first_name(self):
        return self._first

    @property
    def last_name(self):
        return self._last

    @property
    def full_name(self):
        return f"{self._first} {self._last}"

    @property
    def initials(self):
        return f"{self._first[0]}.{self._last[0]}."

    @property
    def age(self):
        return 2024 - self._birth_year

person = Person("John", "Doe", 1990)
print(f"Name: {person.full_name}")
print(f"Initials: {person.initials}")
print(f"Age: {person.age}")

print("\n=== Conditional Properties ===")

class Order:
    def __init__(self, items, discount=0):
        self._items = items
        self._discount = discount

    @property
    def subtotal(self):
        return sum(item["price"] * item["qty"] for item in self._items)

    @property
    def discount_amount(self):
        if self.subtotal > 100:  # Only if over $100
            return self.subtotal * (self._discount / 100)
        return 0

    @property
    def total(self):
        return self.subtotal - self.discount_amount

    @property
    def is_eligible_for_discount(self):
        return self.subtotal > 100

items = [
    {"name": "Book", "price": 25, "qty": 2},
    {"name": "Pen", "price": 5, "qty": 10}
]
order = Order(items, discount=10)

print(f"Subtotal: ${order.subtotal}")
print(f"Eligible for discount: {order.is_eligible_for_discount}")
print(f"Discount (10%): ${order.discount_amount}")
print(f"Total: ${order.total}")
  1. print("=== Read-Only Properties === ")

    3print("=== Read-Only Properties ===\n")45class ImmutablePoint:6    def __init__(self, x, y):7        self._x = x  #?storeprivate8        self._y = y9    10    @property11    def x(self):  #?readonly12        return self._x13    14    @property15    def y(self):16        return self._y17    18    # No setters! Can't modify x or y  #?nosetter1920point = ImmutablePoint(3, 4)21print(f"Point: ({point.x}, {point.y})")
    output=== Read-Only Properties ===
  2. self._x ← 3, self._y ← 4

    5class ImmutablePoint:6    def __init__(self⟨ImmutablePoint A⟩, x3, y4):7        self._x→ 3 = x3  #?storeprivate8        self._y→ 4 = y4
  3. point ← ⟨ImmutablePoint A⟩

    20point→ ⟨ImmutablePoint A⟩ = ImmutablePoint(3, 4)21print(f"Point: ({point.x3}, {point.y4})")
  4. def x(self): #?readonly

    pass 1 of 2
    10@property11def x(self⟨ImmutablePoint A⟩):  #?readonly12    return self._x3
  5. def y(self):

    14@property15def y(self⟨ImmutablePoint A⟩):16    return self._y4
  6. print(f"Point: ({point.x}, {point.y})")

    20point = ImmutablePoint(3, 4)21print(f"Point: ({point.x3}, {point.y4})")2223# Reading works24print(f"X coordinate: {point.x3}")
    outputPoint: (3, 4)
  7. def x(self): #?readonly

    pass 2 of 2
    10@property11def x(self⟨ImmutablePoint A⟩):  #?readonly12    return self._x3
  8. print(f"X coordinate: {point.x}")

    23# Reading works24print(f"X coordinate: {point.x3}")
    outputX coordinate: 3
  9. except AttributeError as e:

    28    point.x = 10  # AttributeError!29except AttributeError as e:30    print(f"Error: can't set x - {eproperty 'x' of 'ImmutablePoint' object has no setter}")
    outputError: can't set x - property 'x' of 'ImmutablePoint' object has no setter
  10. print(" === Computed Read-Only Properties ===")

    32print("\n=== Computed Read-Only Properties ===")3334class Circle:35    def __init__(self, radius):36        self._radius = radius37    38    @property39    def radius(self):40        return self._radius41    42    @radius.setter43    def radius(self, value):44        if value > 0:45            self._radius = value46    47    @property48    def diameter(self):  #?computed149        return self._radius * 250    51    @property52    def circumference(self):  #?computed253        import math54        return 2 * math.pi * self._radius55    56    @property57    def area(self):58        import math59        return math.pi * self._radius ** 26061circle = Circle(5)62print(f"Radius: {circle.radius}")
    output
    === Computed Read-Only Properties ===
  11. self._radius ← 5

    34class Circle:35    def __init__(self⟨Circle B⟩, radius5):36        self._radius→ 5 = radius5
  12. circle ← ⟨Circle B⟩

    61circle→ ⟨Circle B⟩ = Circle(5)62print(f"Radius: {circle.radius5}")63print(f"Diameter: {circle.diameter}")
  13. def radius(self):

    38@property39def radius(self⟨Circle B⟩):40    return self._radius5
  14. print(f"Radius: {circle.radius}")

    61circle = Circle(5)62print(f"Radius: {circle.radius5}")63print(f"Diameter: {circle.diameter10}")64print(f"Circumference: {circle.circumference:.2f}")
    outputRadius: 5
  15. def diameter(self): #?computed1

    pass 1 of 2
    47@property48def diameter(self⟨Circle B⟩):  #?computed149    return self._radius5 * 2
  16. print(f"Diameter: {circle.diameter}")

    62print(f"Radius: {circle.radius}")63print(f"Diameter: {circle.diameter10}")64print(f"Circumference: {circle.circumference31.41592653589793:.2f}")65print(f"Area: {circle.area:.2f}")
    outputDiameter: 10
  17. def circumference(self): #?computed2

    51@property52def circumference(self⟨Circle B⟩):  #?computed253    import math54    return 2 * math.pi3.141592653589793 * self._radius5
  18. print(f"Circumference: {circle.circumference:.2f}")

    63print(f"Diameter: {circle.diameter}")64print(f"Circumference: {circle.circumference31.41592653589793:.2f}")65print(f"Area: {circle.area78.53981633974483:.2f}")
    outputCircumference: 31.42
  19. def area(self):

    pass 1 of 2
    56@property57def area(self⟨Circle B⟩):58    import math59    return math.pi3.141592653589793 * self._radius5 ** 2
  20. print(f"Area: {circle.area:.2f}")

    64print(f"Circumference: {circle.circumference:.2f}")65print(f"Area: {circle.area78.53981633974483:.2f}")6667# Change radius - computed properties update  #?autoupdate68circle.radius = 1069print(f"\nAfter radius = 10:")
    outputArea: 78.54
  21. def radius(self, value):

    42@radius.setter43def radius(self⟨Circle B⟩, value10):44    if value > 0:45        self._radius = value
  22. self._radius ← 10

    43def radius(self, value):44    if value10 > 0:45        self._radius→ 10 = value10
  23. circle.radius ← 10

    67# Change radius - computed properties update  #?autoupdate68circle.radius→ 10 = 1069print(f"\nAfter radius = 10:")70print(f"Diameter: {circle.diameter20}")71print(f"Area: {circle.area:.2f}")
    output
    After radius = 10:
  24. def diameter(self): #?computed1

    pass 2 of 2
    47@property48def diameter(self⟨Circle B⟩):  #?computed149    return self._radius10 * 2
  25. print(f"Diameter: {circle.diameter}")

    69print(f"\nAfter radius = 10:")70print(f"Diameter: {circle.diameter20}")71print(f"Area: {circle.area314.1592653589793:.2f}")
    outputDiameter: 20
  26. def area(self):

    pass 2 of 2
    56@property57def area(self⟨Circle B⟩):58    import math59    return math.pi3.141592653589793 * self._radius10 ** 2
  27. _next_id ← (empty)

    70print(f"Diameter: {circle.diameter}")71print(f"Area: {circle.area314.1592653589793:.2f}")7273print("\n=== ID and Timestamp Properties ===")747576class Document:77    _next_id→ (empty) = 178    79    def __init__(self, title, content):80        self._id = Document._next_id  #?setonce81        Document._next_id += 182        self._created = 1736937000.083        self._title = title84        self._content = content85    86    @property87    def id(self):  #?idreadonly88        return self._id89    90    @property91    def created(self):  #?createdreadonly92        return self._created93    94    @property95    def title(self):96        return self._title97    98    @title.setter  #?titlewritable99    def title(self, value):100        self._title = value101    102    @property103    def content(self):104        return self._content105    106    @content.setter107    def content(self, value):108        self._content = value109110doc = Document("Report", "Initial content")111print(f"ID: {doc.id}")
    outputArea: 314.16
    
    === ID and Timestamp Properties ===
  28. self._id ← 1, Document._next_id ← 2, self._created ← 1736937000.0

    79def __init__(self⟨Document C⟩, titleReport, contentInitial content):80    self._id→ 1 = Document._next_id1  #?setonce81    Document._next_id→ 2 += 182    self._created→ 1736937000.0 = 1736937000.083    self._title→ Report = titleReport84    self._content→ Initial content = contentInitial content
  29. doc ← ⟨Document C⟩

    110doc→ ⟨Document C⟩ = Document("Report", "Initial content")111print(f"ID: {doc.id1}")112print(f"Created: {doc.created}")
  30. def id(self): #?idreadonly

    86@property87def id(self⟨Document C⟩):  #?idreadonly88    return self._id1
  31. print(f"ID: {doc.id}")

    110doc = Document("Report", "Initial content")111print(f"ID: {doc.id1}")112print(f"Created: {doc.created1736937000.0}")113print(f"Title: {doc.title}")
    outputID: 1
  32. def created(self): #?createdreadonly

    90@property91def created(self⟨Document C⟩):  #?createdreadonly92    return self._created1736937000.0
  33. print(f"Created: {doc.created}")

    111print(f"ID: {doc.id}")112print(f"Created: {doc.created1736937000.0}")113print(f"Title: {doc.titleReport}")
    outputCreated: 1736937000.0
  34. def title(self):

    pass 1 of 2
    94@property95def title(self⟨Document C⟩):96    return self._titleReport
  35. print(f"Title: {doc.title}")

    112print(f"Created: {doc.created}")113print(f"Title: {doc.titleReport}")114115# Can modify title and content116doc.title = "Updated Report"117print(f"New title: {doc.title}")
    outputTitle: Report
  36. self._title ← Updated Report

    98@title.setter  #?titlewritable99def title(self⟨Document C⟩, valueUpdated Report):100    self._title→ Updated Report = valueUpdated Report
  37. doc.title ← Updated Report

    115# Can modify title and content116doc.title→ Updated Report = "Updated Report"117print(f"New title: {doc.titleUpdated Report}")
  38. def title(self):

    pass 2 of 2
    94@property95def title(self⟨Document C⟩):96    return self._titleUpdated Report
  39. print(f"New title: {doc.title}")

    116doc.title = "Updated Report"117print(f"New title: {doc.titleUpdated Report}")
    outputNew title: Updated Report
  40. except AttributeError:

    121    doc.id = 999122except AttributeError:123    print("Can't change ID!")
    outputCan't change ID!
  41. print(" === Derived Properties ===")

    125print("\n=== Derived Properties ===")126127class Person:128    def __init__(self, first_name, last_name, birth_year):129        self._first = first_name130        self._last = last_name131        self._birth_year = birth_year132    133    @property134    def first_name(self):135        return self._first136    137    @property138    def last_name(self):139        return self._last140    141    @property142    def full_name(self):  #?derived143        return f"{self._first} {self._last}"144    145    @property146    def initials(self):147        return f"{self._first[0]}.{self._last[0]}."148    149    @property150    def age(self):  #?agecomputed151        return 2024 - self._birth_year152153person = Person("John", "Doe", 1990)154print(f"Name: {person.full_name}")
    output
    === Derived Properties ===
  42. self._first ← John, self._last ← Doe, self._birth_year ← 1990

    127class Person:128    def __init__(self⟨Person D⟩, first_nameJohn, last_nameDoe, birth_year1990):129        self._first→ John = first_nameJohn130        self._last→ Doe = last_nameDoe131        self._birth_year→ 1990 = birth_year1990
  43. person ← ⟨Person D⟩

    153person→ ⟨Person D⟩ = Person("John", "Doe", 1990)154print(f"Name: {person.full_nameJohn Doe}")155print(f"Initials: {person.initials}")
  44. def full_name(self): #?derived

    141@property142def full_name(self⟨Person D⟩):  #?derived143    return f"{self._firstJohn} {self._lastDoe}"
  45. print(f"Name: {person.full_name}")

    153person = Person("John", "Doe", 1990)154print(f"Name: {person.full_nameJohn Doe}")155print(f"Initials: {person.initialsJ.D.}")156print(f"Age: {person.age}")
    outputName: John Doe
  46. def initials(self):

    145@property146def initials(self⟨Person D⟩):147    return f"{self._first[0]J}.{self._last[0]D}."
  47. print(f"Initials: {person.initials}")

    154print(f"Name: {person.full_name}")155print(f"Initials: {person.initialsJ.D.}")156print(f"Age: {person.age34}")
    outputInitials: J.D.
  48. def age(self): #?agecomputed

    149@property150def age(self⟨Person D⟩):  #?agecomputed151    return 2024 - self._birth_year1990
  49. items ← [{'name': 'Book', 'price': 25, 'qty': 2}, {'name': 'Pen', 'price': 5, 'qty': 10}]

    155print(f"Initials: {person.initials}")156print(f"Age: {person.age34}")157158print("\n=== Conditional Properties ===")159160class Order:161    def __init__(self, items, discount=0):162        self._items = items163        self._discount = discount164    165    @property166    def subtotal(self):167        return sum(item["price"] * item["qty"] for item in self._items)168    169    @property170    def discount_amount(self):  #?conditional171        if self.subtotal > 100:  # Only if over $100  #?thresholdcheck172            return self.subtotal * (self._discount / 100)173        return 0174    175    @property176    def total(self):177        return self.subtotal - self.discount_amount178    179    @property180    def is_eligible_for_discount(self):  #?boolprop181        return self.subtotal > 100182183items→ [{'name': 'Book', 'price': 25, 'qty': 2}, {'name': 'Pen', 'price': 5, 'qty': 10}] = [184    {"name": "Book", "price": 25, "qty": 2},185    {"name": "Pen", "price": 5, "qty": 10}186]187order = Order(items[{'name': 'Book', 'price': 25, 'qty': 2}, {'name': 'Pen', 'price': 5, 'qty': 10}], discount=10)
    outputAge: 34
    
    === Conditional Properties ===
  50. self._items ← [{'name': 'Book', 'price': 25, 'qty': 2}, {'name': 'Pen', 'price': 5, 'qty': 10}]

    160class Order:161    def __init__(self⟨Order E⟩, items[{'name': 'Book', 'price': 25, 'qty': 2}, {'name': 'Pen', 'price': 5, 'qty': 10}], discount10=0):162        self._items→ [{'name': 'Book', 'price': 25, 'qty': 2}, {'name': 'Pen', 'price': 5, 'qty': 10}] = items[{'name': 'Book', 'price': 25, 'qty': 2}, {'name': 'Pen', 'price': 5, 'qty': 10}]163        self._discount→ 10 = discount10
  51. order ← ⟨Order E⟩

    186]187order→ ⟨Order E⟩ = Order(items[{'name': 'Book', 'price': 25, 'qty': 2}, {'name': 'Pen', 'price': 5, 'qty': 10}], discount=10)188189print(f"Subtotal: ${order.subtotal100}")190print(f"Eligible for discount: {order.is_eligible_for_discount}")
  52. def subtotal(self):

    pass 1 of 5
    165@property166def subtotal(self⟨Order E⟩):167    return sum(item["price"](empty) * item["qty"](empty) for item in self._items[{'name': 'Book', 'price': 25, 'qty': 2}, {'name': 'Pen', 'price': 5, 'qty': 10}])
  53. print(f"Subtotal: ${order.subtotal}")

    189print(f"Subtotal: ${order.subtotal100}")190print(f"Eligible for discount: {order.is_eligible_for_discountFalse}")191print(f"Discount (10%): ${order.discount_amount}")
    outputSubtotal: $100
  54. def is_eligible_for_discount(self): #?boolprop

    179@property180def is_eligible_for_discount(self⟨Order E⟩):  #?boolprop181    return self.subtotal100 > 100
  55. print(f"Eligible for discount: {order.is_eligible_for_discount}")

    189print(f"Subtotal: ${order.subtotal}")190print(f"Eligible for discount: {order.is_eligible_for_discountFalse}")191print(f"Discount (10%): ${order.discount_amount0}")192print(f"Total: ${order.total}")
    outputEligible for discount: False
  56. def discount_amount(self): #?conditional

    pass 1 of 2
    169@property170def discount_amount(self⟨Order E⟩):  #?conditional171    if self.subtotal > 100:  # Only if over $100  #?thresholdcheck172        return self.subtotal * (self._discount / 100)
  57. return 0

    172    return self.subtotal * (self._discount / 100)173return 0
  58. print(f"Discount (10%): ${order.discount_amount}")

    190print(f"Eligible for discount: {order.is_eligible_for_discount}")191print(f"Discount (10%): ${order.discount_amount0}")192print(f"Total: ${order.total100}")193#@help storeprivate
    outputDiscount (10%): $0
  59. def total(self):

    175@property176def total(self⟨Order E⟩):177    return self.subtotal100 - self.discount_amount0
  60. def discount_amount(self): #?conditional

    pass 2 of 2
    169@property170def discount_amount(self⟨Order E⟩):  #?conditional171    if self.subtotal > 100:  # Only if over $100  #?thresholdcheck172        return self.subtotal * (self._discount / 100)
  61. return 0

    172    return self.subtotal * (self._discount / 100)173return 0
  62. print(f"Total: ${order.total}")

    191print(f"Discount (10%): ${order.discount_amount}")192print(f"Total: ${order.total100}")193#@help storeprivate
    outputTotal: $100

Provide getter without setter. Attempts to set raise AttributeError.

Properties with validation

Validate data before setting.

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

print("=== Type Validation ===\n")

class Product:
    def __init__(self, name, price):
        self._name = None
        self._price = None
        # Use setters for validation
        self.name = name
        self.price = price

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("Name must be a string")
        if len(value) < 1:
            raise ValueError("Name cannot be empty")
        self._name = value

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError("Price must be a number")
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price = round(value, 2)

product = Product("Laptop", 999.999)
print(f"Product: {product.name}, ${product.price}")

# Validation in action
try:
    product.price = "free"
except TypeError as e:
    print(f"Type error: {e}")

try:
    product.price = -50
except ValueError as e:
    print(f"Value error: {e}")

print("\n=== Range Validation ===")

class Temperature:
    MIN_CELSIUS = -273.15
    MAX_CELSIUS = 1000

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

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

    @celsius.setter
    def celsius(self, value):
        if value < self.MIN_CELSIUS:
            raise ValueError(f"Below absolute zero: {value}")
        if value > self.MAX_CELSIUS:
            raise ValueError(f"Too hot: {value}")
        self._celsius = value

temp = Temperature(25)
print(f"Temperature: {temp.celsius}°C")

try:
    temp.celsius = -300  # Below absolute zero
except ValueError as e:
    print(f"Rejected: {e}")

print("\n=== Format Validation ===")

class User:
    def __init__(self, email, phone):
        self._email = None
        self._phone = None
        self.email = email
        self.phone = phone

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

    @email.setter
    def email(self, value):
        value = value.strip().lower()
        if "@" not in value:
            raise ValueError("Invalid email: missing @")
        parts = value.split("@")
        if len(parts) != 2 or not parts[1]:
            raise ValueError("Invalid email format")
        self._email = value

    @property
    def phone(self):
        return self._phone

    @phone.setter
    def phone(self, value):
        # Remove non-digits
        digits = ''.join(c for c in value if c.isdigit())
        if len(digits) < 10:
            raise ValueError("Phone must have at least 10 digits")
        self._phone = digits

user = User("  Alice@Example.COM  ", "(555) 123-4567")
print(f"Email: {user.email}")
print(f"Phone: {user.phone}")

print("\n=== Interdependent Validation ===")

class Rectangle:
    def __init__(self, width, height, max_area=10000):
        self._width = 0
        self._height = 0
        self._max_area = max_area
        self.width = width  # Validates
        self.height = height  # Validates

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("Width must be positive")
        if value * self._height > self._max_area:
            raise ValueError(f"Area would exceed {self._max_area}")
        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")
        if self._width * value > self._max_area:
            raise ValueError(f"Area would exceed {self._max_area}")
        self._height = value

    @property
    def area(self):
        return self._width * self._height

rect = Rectangle(50, 100)
print(f"Rectangle: {rect.width}x{rect.height}, area={rect.area}")

try:
    rect.width = 200  # Would make area 20000
except ValueError as e:
    print(f"Rejected: {e}")

print("\n=== Validation with Warnings ===")

class Score:
    def __init__(self, value):
        self._value = 0
        self._warnings = []
        self.value = value

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        self._warnings.clear()

        if value < 0:
            self._warnings.append(f"Adjusted {value} to 0")
            value = 0
        elif value > 100:
            self._warnings.append(f"Adjusted {value} to 100")
            value = 100

        self._value = value

    @property
    def warnings(self):
        return self._warnings.copy()

score = Score(150)
print(f"Score: {score.value}")
print(f"Warnings: {score.warnings}")

score.value = -20
print(f"Score: {score.value}")
print(f"Warnings: {score.warnings}")
  1. print("=== Type Validation === ")

    3print("=== Type Validation ===\n")45class Product:6    def __init__(self, name, price):7        self._name = None8        self._price = None9        # Use setters for validation  #?usesetters10        self.name = name11        self.price = price12    13    @property14    def name(self):15        return self._name16    17    @name.setter18    def name(self, value):19        if not isinstance(value, str):  #?typecheck20            raise TypeError("Name must be a string")21        if len(value) < 1:22            raise ValueError("Name cannot be empty")23        self._name = value24    25    @property26    def price(self):27        return self._price28    29    @price.setter30    def price(self, value):31        if not isinstance(value, (int, float)):  #?numcheck32            raise TypeError("Price must be a number")33        if value < 0:34            raise ValueError("Price cannot be negative")35        self._price = round(value, 2)  #?normalize3637product = Product("Laptop", 999.999)38print(f"Product: {product.name}, ${product.price}")
    output=== Type Validation ===
  2. self._name ← None, self._price ← None

    5class Product:6    def __init__(self⟨Product A⟩, nameLaptop, price999.999):7        self._name→ None = None8        self._price→ None = None9        # Use setters for validation  #?usesetters10        self.name = nameLaptop11        self.price = price
  3. self._name ← Laptop, self.name ← Laptop

    9    # Use setters for validation  #?usesetters10    self.name→ Laptop = nameLaptop11    self.price = price999.9991213@property14def name(self):15    return self._name1617@name.setter18def name(self⟨Product A⟩, valueLaptop):19    if not isinstance(value, str):  #?typecheck20        raise TypeError("Name must be a string")21    if len(value) < 1:22        raise ValueError("Name cannot be empty")23    self._name→ Laptop = valueLaptop
  4. self._price ← 1000.0, self.price ← 1000.0

    pass 1 of 3
    10    self.name = name11    self.price→ 1000.0 = price999.9991213@property14def name(self):15    return self._name1617@name.setter18def name(self, value):19    if not isinstance(value, str):  #?typecheck20        raise TypeError("Name must be a string")21    if len(value) < 1:22        raise ValueError("Name cannot be empty")23    self._name = value2425@property26def price(self):27    return self._price2829@price.setter30def price(self⟨Product A⟩, value999.999):31    if not isinstance(value, (int, float)):  #?numcheck32        raise TypeError("Price must be a number")33    if value < 0:34        raise ValueError("Price cannot be negative")35    self._price→ 1000.0 = round(value999.999, 2)  #?normalize
    All 3 passes — pass 1 is the card above
    passvaluepriceeself._priceself.price
    1999.999999.9991000.01000.0
    2freePrice must be a number
    3-50Price cannot be negative
  5. product ← ⟨Product A⟩

    37product→ ⟨Product A⟩ = Product("Laptop", 999.999)38print(f"Product: {product.nameLaptop}, ${product.price1000.0}")
  6. def name(self):

    13@property14def name(self⟨Product A⟩):15    return self._nameLaptop
  7. def price(self):

    25@property26def price(self⟨Product A⟩):27    return self._price1000.0
  8. print(f"Product: {product.name}, ${product.price}")

    37product = Product("Laptop", 999.999)38print(f"Product: {product.nameLaptop}, ${product.price1000.0}")
    outputProduct: Laptop, $1000.0
  9. if not isinstance(value, (int, float)): #?numcheck

    30def price(self, value):31    if not isinstance(valuefree, (int, float)):  #?numcheck32        raise TypeError("Price must be a number")33    if value < 0:
  10. except TypeError as e:

    42    product.price = "free"43except TypeError as e:44    print(f"Type error: {ePrice must be a number}")
    outputType error: Price must be a number
  11. if value < 0:

    32    raise TypeError("Price must be a number")33if value-50 < 0:34    raise ValueError("Price cannot be negative")35self._price = round(value, 2)  #?normalize
  12. except ValueError as e:

    47    product.price = -5048except ValueError as e:49    print(f"Value error: {ePrice cannot be negative}")
    outputValue error: Price cannot be negative
  13. MIN_CELSIUS ← (empty), MAX_CELSIUS ← (empty)

    51print("\n=== Range Validation ===")5253class Temperature:54    MIN_CELSIUS→ (empty) = -273.15  #?classconst55    MAX_CELSIUS→ (empty) = 100056    57    def __init__(self, celsius):58        self._celsius = None59        self.celsius = celsius60    61    @property62    def celsius(self):63        return self._celsius64    65    @celsius.setter66    def celsius(self, value):67        if value < self.MIN_CELSIUS:  #?rangecheck68            raise ValueError(f"Below absolute zero: {value}")69        if value > self.MAX_CELSIUS:70            raise ValueError(f"Too hot: {value}")71        self._celsius = value7273temp = Temperature(25)74print(f"Temperature: {temp.celsius}°C")
    output
    === Range Validation ===
  14. self._celsius ← None

    57def __init__(self⟨Temperature B⟩, celsius25):58    self._celsius→ None = None59    self.celsius = celsius25
  15. self._celsius ← 25, self.celsius ← 25

    pass 1 of 2
    58    self._celsius = None59    self.celsius→ 25 = celsius256061@property62def celsius(self):63    return self._celsius6465@celsius.setter66def celsius(self⟨Temperature B⟩, value25):67    if value < self.MIN_CELSIUS:  #?rangecheck68        raise ValueError(f"Below absolute zero: {value}")69    if value > self.MAX_CELSIUS:70        raise ValueError(f"Too hot: {value}")71    self._celsius→ 25 = value25
  16. temp ← ⟨Temperature B⟩

    73temp→ ⟨Temperature B⟩ = Temperature(25)74print(f"Temperature: {temp.celsius25}°C")
  17. def celsius(self):

    61@property62def celsius(self⟨Temperature B⟩):63    return self._celsius25
  18. print(f"Temperature: {temp.celsius}°C")

    73temp = Temperature(25)74print(f"Temperature: {temp.celsius25}°C")
    outputTemperature: 25°C
  19. def celsius(self, value):

    pass 2 of 2
    65@celsius.setter66def celsius(self⟨Temperature B⟩, value-300):67    if value < self.MIN_CELSIUS:  #?rangecheck68        raise ValueError(f"Below absolute zero: {value}")
  20. if value < self.MIN_CELSIUS: #?rangecheck

    66def celsius(self, value):67    if value-300 < self.MIN_CELSIUS-273.15:  #?rangecheck68        raise ValueError(f"Below absolute zero: {value-300}")69    if value > self.MAX_CELSIUS:
  21. except ValueError as e:

    77    temp.celsius = -300  # Below absolute zero78except ValueError as e:79    print(f"Rejected: {eBelow absolute zero: -300}")
    outputRejected: Below absolute zero: -300
  22. print(" === Format Validation ===")

    81print("\n=== Format Validation ===")8283class User:84    def __init__(self, email, phone):85        self._email = None86        self._phone = None87        self.email = email88        self.phone = phone89    90    @property91    def email(self):92        return self._email93    94    @email.setter95    def email(self, value):96        value = value.strip().lower()  #?normalize297        if "@" not in value:  #?emailformat98            raise ValueError("Invalid email: missing @")99        parts = value.split("@")100        if len(parts) != 2 or not parts[1]:101            raise ValueError("Invalid email format")102        self._email = value103    104    @property105    def phone(self):106        return self._phone107    108    @phone.setter109    def phone(self, value):110        # Remove non-digits  #?phoneformat111        digits = ''.join(c for c in value if c.isdigit())112        if len(digits) < 10:113            raise ValueError("Phone must have at least 10 digits")114        self._phone = digits115116user = User("  Alice@Example.COM  ", "(555) 123-4567")117print(f"Email: {user.email}")
    output
    === Format Validation ===
  23. self._email ← None, self._phone ← None

    83class User:84    def __init__(self⟨User C⟩, email  Alice@Example.COM  , phone(555) 123-4567):85        self._email→ None = None86        self._phone→ None = None87        self.email = email  Alice@Example.COM  88        self.phone = phone
  24. value ← alice@example.com, parts ← ['alice', 'example.com'], self._email ← alice@example.com

    86    self._phone = None87    self.email→ alice@example.com = email  Alice@Example.COM  88    self.phone = phone(555) 123-45678990@property91def email(self):92    return self._email9394@email.setter95def email(self⟨User C⟩, value  Alice@Example.COM  ):96    value→ alice@example.com = value.strip().lower()  #?normalize297    if "@" not in value:  #?emailformat98        raise ValueError("Invalid email: missing @")99    parts→ ['alice', 'example.com'] = valuealice@example.com.split("@")100    if len(parts) != 2 or not parts[1]:101        raise ValueError("Invalid email format")102    self._email→ alice@example.com = valuealice@example.com
  25. digits ← 5551234567, self._phone ← 5551234567, self.phone ← 5551234567

    87    self.email = email88    self.phone→ 5551234567 = phone(555) 123-45678990@property91def email(self):92    return self._email9394@email.setter95def email(self, value):96    value = value.strip().lower()  #?normalize297    if "@" not in value:  #?emailformat98        raise ValueError("Invalid email: missing @")99    parts = value.split("@")100    if len(parts) != 2 or not parts[1]:101        raise ValueError("Invalid email format")102    self._email = value103104@property105def phone(self):106    return self._phone107108@phone.setter109def phone(self⟨User C⟩, value(555) 123-4567):110    # Remove non-digits  #?phoneformat111    digits→ 5551234567 = ''.join(c for c in value(555) 123-4567 if c.isdigit())112    if len(digits) < 10:113        raise ValueError("Phone must have at least 10 digits")114    self._phone→ 5551234567 = digits5551234567
  26. user ← ⟨User C⟩

    116user→ ⟨User C⟩ = User("  Alice@Example.COM  ", "(555) 123-4567")117print(f"Email: {user.emailalice@example.com}")118print(f"Phone: {user.phone}")
  27. def email(self):

    90@property91def email(self⟨User C⟩):92    return self._emailalice@example.com
  28. print(f"Email: {user.email}")

    116user = User("  Alice@Example.COM  ", "(555) 123-4567")117print(f"Email: {user.emailalice@example.com}")118print(f"Phone: {user.phone5551234567}")
    outputEmail: alice@example.com
  29. def phone(self):

    104@property105def phone(self⟨User C⟩):106    return self._phone5551234567
  30. print(f"Phone: {user.phone}")

    117print(f"Email: {user.email}")118print(f"Phone: {user.phone5551234567}")119120print("\n=== Interdependent Validation ===")121122class Rectangle:123    def __init__(self, width, height, max_area=10000):124        self._width = 0125        self._height = 0126        self._max_area = max_area  #?constraint127        self.width = width  # Validates128        self.height = height  # Validates129    130    @property131    def width(self):132        return self._width133    134    @width.setter135    def width(self, value):136        if value <= 0:137            raise ValueError("Width must be positive")138        if value * self._height > self._max_area:  #?crosscheck139            raise ValueError(f"Area would exceed {self._max_area}")140        self._width = value141    142    @property143    def height(self):144        return self._height145    146    @height.setter147    def height(self, value):148        if value <= 0:149            raise ValueError("Height must be positive")150        if self._width * value > self._max_area:151            raise ValueError(f"Area would exceed {self._max_area}")152        self._height = value153    154    @property155    def area(self):156        return self._width * self._height157158rect = Rectangle(50, 100)159print(f"Rectangle: {rect.width}x{rect.height}, area={rect.area}")
    outputPhone: 5551234567
    
    === Interdependent Validation ===
  31. self._width ← 0, self._height ← 0, self._max_area ← 10000

    122class Rectangle:123    def __init__(self⟨Rectangle D⟩, width50, height100, max_area10000=10000):124        self._width→ 0 = 0125        self._height→ 0 = 0126        self._max_area→ 10000 = max_area10000  #?constraint127        self.width = width50  # Validates128        self.height = height  # Validates
  32. self._width ← 50, self.width ← 50

    pass 1 of 2
    126    self._max_area = max_area  #?constraint127    self.width→ 50 = width50  # Validates128    self.height = height100  # Validates129130@property131def width(self):132    return self._width133134@width.setter135def width(self⟨Rectangle D⟩, value50):136    if value <= 0:137        raise ValueError("Width must be positive")138    if value * self._height > self._max_area:  #?crosscheck139        raise ValueError(f"Area would exceed {self._max_area}")140    self._width→ 50 = value50
  33. self._height ← 100, self.height ← 100

    127    self.width = width  # Validates128    self.height→ 100 = height100  # Validates129130@property131def width(self):132    return self._width133134@width.setter135def width(self, value):136    if value <= 0:137        raise ValueError("Width must be positive")138    if value * self._height > self._max_area:  #?crosscheck139        raise ValueError(f"Area would exceed {self._max_area}")140    self._width = value141142@property143def height(self):144    return self._height145146@height.setter147def height(self⟨Rectangle D⟩, value100):148    if value <= 0:149        raise ValueError("Height must be positive")150    if self._width * value > self._max_area:151        raise ValueError(f"Area would exceed {self._max_area}")152    self._height→ 100 = value100
  34. rect ← ⟨Rectangle D⟩

    158rect→ ⟨Rectangle D⟩ = Rectangle(50, 100)159print(f"Rectangle: {rect.width50}x{rect.height100}, area={rect.area5000}")
  35. def width(self):

    130@property131def width(self⟨Rectangle D⟩):132    return self._width50
  36. def height(self):

    142@property143def height(self⟨Rectangle D⟩):144    return self._height100
  37. def area(self):

    154@property155def area(self⟨Rectangle D⟩):156    return self._width50 * self._height100
  38. print(f"Rectangle: {rect.width}x{rect.height}, area={rect.area}")

    158rect = Rectangle(50, 100)159print(f"Rectangle: {rect.width50}x{rect.height100}, area={rect.area5000}")
    outputRectangle: 50x100, area=5000
  39. def width(self, value):

    pass 2 of 2
    134@width.setter135def width(self⟨Rectangle D⟩, value200):136    if value <= 0:137        raise ValueError("Width must be positive")
  40. if value * self._height > self._max_area: #?crosscheck

    137    raise ValueError("Width must be positive")138if value200 * self._height100 > self._max_area10000:  #?crosscheck139    raise ValueError(f"Area would exceed {self._max_area10000}")140self._width = value
  41. except ValueError as e:

    162    rect.width = 200  # Would make area 20000  #?exceedmax163except ValueError as e:164    print(f"Rejected: {eArea would exceed 10000}")
    outputRejected: Area would exceed 10000
  42. print(" === Validation with Warnings ===")

    166print("\n=== Validation with Warnings ===")167168class Score:169    def __init__(self, value):170        self._value = 0171        self._warnings = []  #?trackwarnings172        self.value = value173    174    @property175    def value(self):176        return self._value177    178    @value.setter179    def value(self, value):180        self._warnings.clear()181        182        if value < 0:  #?autocorrect183            self._warnings.append(f"Adjusted {value} to 0")184            value = 0185        elif value > 100:186            self._warnings.append(f"Adjusted {value} to 100")187            value = 100188        189        self._value = value190    191    @property192    def warnings(self):  #?getwarnings193        return self._warnings.copy()194195score = Score(150)196print(f"Score: {score.value}")
    output
    === Validation with Warnings ===
  43. self._value ← 0, self._warnings ← []

    168class Score:169    def __init__(self⟨Score E⟩, value150):170        self._value→ 0 = 0171        self._warnings→ [] = []  #?trackwarnings172        self.value = value150
  44. def value(self, value):

    pass 1 of 2
    178@value.setter179def value(self⟨Score E⟩, value150):180    self._warnings[].clear()
  45. self._warnings ← ['Adjusted 150 to 100'], value ← 100

    184    value = 0185elif value150 > 100:186    self._warnings→ ['Adjusted 150 to 100'].append(f"Adjusted {value150} to 100")187    value→ 100 = 100
  46. self._value ← 100, self.value ← 100

    171    self._warnings = []  #?trackwarnings172    self.value→ 100 = value150173174@property175def value(self):176    return self._value177178@value.setter179def value(self, value):180    self._warnings.clear()181    182    if value < 0:  #?autocorrect183        self._warnings.append(f"Adjusted {value} to 0")184        value = 0185    elif value > 100:186        self._warnings.append(f"Adjusted {value} to 100")187        value = 100188    189    self._value→ 100 = value100
  47. score ← ⟨Score E⟩

    195score→ ⟨Score E⟩ = Score(150)196print(f"Score: {score.value100}")197print(f"Warnings: {score.warnings}")
  48. def value(self):

    pass 1 of 2
    174@property175def value(self⟨Score E⟩):176    return self._value100
  49. print(f"Score: {score.value}")

    195score = Score(150)196print(f"Score: {score.value100}")197print(f"Warnings: {score.warnings['Adjusted 150 to 100']}")
    outputScore: 100
  50. def warnings(self): #?getwarnings

    pass 1 of 2
    191@property192def warnings(self⟨Score E⟩):  #?getwarnings193    return self._warnings['Adjusted 150 to 100'].copy()
  51. print(f"Warnings: {score.warnings}")

    196print(f"Score: {score.value}")197print(f"Warnings: {score.warnings['Adjusted 150 to 100']}")198199score.value = -20200print(f"Score: {score.value}")
    outputWarnings: ['Adjusted 150 to 100']
  52. self._warnings ← []

    pass 2 of 2
    178@value.setter179def value(self⟨Score E⟩, value-20):180    self._warnings→ [].clear()
  53. self._warnings ← ['Adjusted -20 to 0'], value ← 0

    182if value-20 < 0:  #?autocorrect183    self._warnings→ ['Adjusted -20 to 0'].append(f"Adjusted {value-20} to 0")184    value→ 0 = 0185elif value > 100:
  54. self._value ← 0

    189self._value→ 0 = value0
  55. score.value ← 0

    199score.value→ 0 = -20200print(f"Score: {score.value0}")201print(f"Warnings: {score.warnings}")
  56. def value(self):

    pass 2 of 2
    174@property175def value(self⟨Score E⟩):176    return self._value0
  57. print(f"Score: {score.value}")

    199score.value = -20200print(f"Score: {score.value0}")201print(f"Warnings: {score.warnings['Adjusted -20 to 0']}")202#@help usesetters
    outputScore: 0
  58. def warnings(self): #?getwarnings

    pass 2 of 2
    191@property192def warnings(self⟨Score E⟩):  #?getwarnings193    return self._warnings['Adjusted -20 to 0'].copy()
  59. print(f"Warnings: {score.warnings}")

    200print(f"Score: {score.value}")201print(f"Warnings: {score.warnings['Adjusted -20 to 0']}")202#@help usesetters
    outputWarnings: ['Adjusted -20 to 0']

Setter can check value and raise exception if invalid.

Exercise: practical.py

Build a class with encapsulated state and validation