Skip to content

Read-Only Properties

Creating Read-Only

1. Property Without Setter

The simplest way to create a read-only property is to define only the getter:

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

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

2. Attempting to Write

c = Circle(5)
print(c.area)  # ✅ Works: 78.54...
c.area = 100   # ❌ AttributeError: can't set attribute

3. Why Use Read-Only

  • Prevent accidental modification of computed values
  • Enforce immutability for specific attributes
  • Maintain data consistency
  • Express design intent clearly

Common Patterns

1. Computed Values

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

    @property
    def area(self):
        return self.width * self.height

    @property
    def perimeter(self):
        return 2 * (self.width + self.height)

    @property
    def diagonal(self):
        return (self.width ** 2 + self.height ** 2) ** 0.5

2. Derived Attributes

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

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

3. Configuration Values

class Config:
    def __init__(self):
        self._api_key = "secret123"

    @property
    def api_endpoint(self):
        return "https://api.example.com/v1"

    @property
    def max_retries(self):
        return 3

Protecting Internal State

1. Private Attribute Pattern

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

    @property
    def balance(self):
        """Read-only access to balance"""
        return self._balance

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def withdraw(self, amount):
        if 0 < amount <= self._balance:
            self._balance -= amount

2. Why This Works

  • _balance is private (convention)
  • balance property provides read-only access
  • Modifications only through controlled methods
  • Maintains invariants

3. Usage Example

account = BankAccount(1000)
print(account.balance)  # ✅ 1000
account.deposit(500)    # ✅ Works
# account.balance = 5000  # ❌ Can't set attribute

Immutable Objects

1. Full Immutability

class Point:
    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

    @property
    def magnitude(self):
        return (self._x ** 2 + self._y ** 2) ** 0.5

2. Benefits

  • Thread-safe by design
  • Can be used as dictionary keys
  • Easier to reason about
  • Prevents bugs from state mutation

3. Creating New Instances

class Point:
    # ... (properties as above)

    def move(self, dx, dy):
        """Returns new Point with offset"""
        return Point(self._x + dx, self._y + dy)

Advanced Patterns

1. Conditional Read-Only

Make property read-only based on state:

class Document:
    def __init__(self):
        self._content = ""
        self._locked = False

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

    @content.setter
    def content(self, value):
        if self._locked:
            raise AttributeError("Document is locked")
        self._content = value

    def lock(self):
        self._locked = True

2. Lazy Evaluation

class ExpensiveCalculation:
    def __init__(self, data):
        self._data = data
        self._result = None

    @property
    def result(self):
        """Computed once, then cached"""
        if self._result is None:
            print("Computing...")
            self._result = sum(x**2 for x in self._data)
        return self._result

3. Type Conversion

class DataRecord:
    def __init__(self, timestamp_str):
        self._timestamp_str = timestamp_str

    @property
    def timestamp(self):
        """Always returns datetime object"""
        from datetime import datetime
        return datetime.fromisoformat(self._timestamp_str)

Comparison

1. Read-Only vs Writable

Aspect Read-Only Property Writable Property
Setter Not defined Defined
Assignment Raises error Allowed
Use case Computed/protected values Validated attributes
Mutability Immutable Mutable

2. Enforcement Levels

Method Enforcement Access
Public attribute None obj.x = 5 works
_private convention Social obj._x = 5 discouraged
Read-only property Strong obj.x = 5 raises error

3. Design Principles

Use read-only properties when: - Value is derived from other state - External modification would break invariants - Expressing configuration or constants - Implementing immutable data structures