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

Basic property

Turn a method into a read-only attribute.

basics.py
# Basic @property Usage

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

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

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

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

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

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


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

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

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

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

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

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

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


if __name__ == "__main__":
    main()







































# Basic @property Usage

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

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

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

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

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

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


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

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

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

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

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

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

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


if __name__ == "__main__":
    main()







































# Basic @property Usage

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

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

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

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

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

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


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

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

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

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

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

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

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


if __name__ == "__main__":
    main()







































@property decorator makes method accessible like attribute.

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

Property with validation

Validate values before setting.

validation.py
# Properties with Validation

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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


if __name__ == "__main__":
    main()




















































































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

Computed properties

Calculate values on demand.

computed.py
# Computed Properties

import math


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("Width must be positive")
        self._width = value

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

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("Height must be positive")
        self._height = value

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

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

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

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


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

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

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

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

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

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

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

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


if __name__ == "__main__":
    main()


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

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

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

# radius_property
# @property
#
# Radius getter.
#

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

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

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

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

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

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

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

# diameter_property
# @property
#
# Computed diameter.
#

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

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

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

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

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

# area_property
# @property
#
# Computed area.
#

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

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

# circumference_property
# @property
#
# Computed circumference.
#

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

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

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

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

# celsius_property
# @property
#
# Celsius getter.
#

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

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

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

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

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

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

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

# fahrenheit_property
# @property
#
# Computed Fahrenheit.
#

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

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

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

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

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

# kelvin_property
# @property
#
# Computed Kelvin.
#

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

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

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

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

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

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

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

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

# width_property
# @property
#
# Width getter.
#

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

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

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

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

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

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

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

# height_property
# @property
#
# Height getter.
#

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

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

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

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

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

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

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

# area_rect_property
# @property
#
# Computed area.
#

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

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

# perimeter_property
# @property
#
# Computed perimeter.
#

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# print_is_now_square
# print(f"Is square: {rect.is_square}")
#
# "Is square: True"
#

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

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

Read-only properties

Prevent modification after creation.

readonly.py
# Read-only Properties

from datetime import datetime


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

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

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

    # No @birth_year.setter - read-only!

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


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

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

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

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

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


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

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

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

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

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

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

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


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

    def __init__(self):
        self._count = 0

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


if __name__ == "__main__":
    main()


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

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

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

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

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

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

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

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

# age_property
# @property
#
# Computed age.
#

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

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

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

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

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

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

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

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

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

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

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

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

# distance_property
# @property
#
# Computed distance.
#

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Provide getter without setter. Assignment raises AttributeError.

Migrating to properties

Change implementation without breaking API.

migration.py
# Migrating from Direct Attributes to Properties

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

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


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

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

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

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

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

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


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

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

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


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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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


if __name__ == "__main__":
    main()


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

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

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

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

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

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

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

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

# username_property_v2
# @property
#
# Username property.
#

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

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

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

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

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

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

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

# email_property_v2
# @property
#
# Email property.
#

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# total_property
# @property
#
# Computed total.
#

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Exercise: practical.py

Build a temperature class with Celsius/Fahrenheit properties