Skip to main content

Public vs Private Access

Introduction​

One of the core principles of object-oriented programming is encapsulation — hiding the internal details of a class and only exposing what's necessary.

In Python, properties and methods are public by default, meaning they can be accessed from outside the class. Python uses naming conventions to indicate privacy:

  • Single underscore _ prefix: indicates "internal use" (convention only)
  • Double underscore __ prefix: triggers name mangling for stronger privacy
  • @property decorator: provides controlled access through getters and setters

Encapsulation helps:

  • Prevent accidental modification of sensitive data
  • Control how data is read or changed (using properties)
  • Keep internal logic hidden while exposing a clean interface
  • Maintain data integrity through validation

Concepts​

Public Members

  • Default in Python
  • Accessible anywhere
  • No special naming conventions
class Person:
def __init__(self, name):
self.name = name # public

def greet(self): # public method
return f"Hello, I'm {self.name}"

p = Person("Alice")
print(p.name) # Alice ✅
print(p.greet()) # Hello, I'm Alice ✅

Protected Members (Convention)

  • Start with single underscore _
  • Indicates "internal use" - still accessible but discouraged
  • Used for subclass access
class BankAccount:
def __init__(self, owner, starting_balance=0):
self.owner = owner # public
self._balance = starting_balance # protected (convention)

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

def get_balance(self):
return self._balance

acc = BankAccount("Alice", 100)
acc.deposit(50)
print(acc.get_balance()) # 150
print(acc._balance) # 150 ⚠️ Accessible but discouraged

Private Members (Name Mangling)

  • Start with double underscore __
  • Python mangles the name to make it harder to access
  • Provides stronger privacy protection
class SecureBankAccount:
def __init__(self, owner, starting_balance=0):
self.owner = owner # public
self.__balance = starting_balance # private (name mangled)

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

def get_balance(self):
return self.__balance

def __validate_transaction(self, amount): # private method
return amount > 0 and amount <= self.__balance

acc = SecureBankAccount("Alice", 100)
acc.deposit(50)
print(acc.get_balance()) # 150
# print(acc.__balance) # ❌ AttributeError
# print(acc.__validate_transaction(10)) # ❌ AttributeError

Properties (Getters and Setters)

  • Provide controlled access to private attributes
  • Use @property, @attribute.setter, and @attribute.deleter
class Employee:
def __init__(self, name, salary):
self.name = name
self.__salary = salary # private

@property
def salary(self):
"""Getter for salary"""
return self.__salary

@salary.setter
def salary(self, value):
"""Setter for salary with validation"""
if not isinstance(value, (int, float)):
raise TypeError("Salary must be a number")
if value < 0:
raise ValueError("Salary must be positive")
self.__salary = value

@salary.deleter
def salary(self):
"""Deleter for salary"""
self.__salary = 0

emp = Employee("Bob", 50000)
print(emp.salary) # 50000 (calls getter)
emp.salary = 60000 # calls setter
print(emp.salary) # 60000
# emp.salary = -1000 # ❌ ValueError: Salary must be positive

Class Variables vs Instance Variables

  • Class variables are shared among all instances
  • Instance variables are unique to each instance
class Student:
school_name = "Python University" # class variable (public)
_enrollment_count = 0 # class variable (protected)

def __init__(self, name, student_id):
self.name = name # instance variable (public)
self._student_id = student_id # instance variable (protected)
Student._enrollment_count += 1

@classmethod
def get_enrollment_count(cls):
return cls._enrollment_count

@property
def student_id(self):
return self._student_id

s1 = Student("Alice", "S001")
s2 = Student("Bob", "S002")
print(Student.school_name) # Python University
print(Student.get_enrollment_count()) # 2
print(s1.student_id) # S001 (read-only)

Examples​

# Complete example: Smart Home Device
class SmartThermostat:
# Class variables
MIN_TEMP = 10 # public constant
MAX_TEMP = 30 # public constant
_device_count = 0 # protected class variable

def __init__(self, room_name, initial_temp=20):
self.room_name = room_name # public
self._is_online = True # protected
self.__current_temp = initial_temp # private
self.__target_temp = initial_temp # private
SmartThermostat._device_count += 1

@property
def current_temperature(self):
"""Read-only access to current temperature"""
return self.__current_temp

@property
def target_temperature(self):
"""Get target temperature"""
return self.__target_temp

@target_temperature.setter
def target_temperature(self, temp):
"""Set target temperature with validation"""
if not isinstance(temp, (int, float)):
raise TypeError("Temperature must be a number")
if not (self.MIN_TEMP <= temp <= self.MAX_TEMP):
raise ValueError(f"Temperature must be between {self.MIN_TEMP}°C and {self.MAX_TEMP}°C")

self.__target_temp = temp
self.__adjust_temperature()

def __adjust_temperature(self):
"""Private method to adjust current temperature"""
if self.__current_temp < self.__target_temp:
self.__current_temp = min(self.__target_temp, self.__current_temp + 1)
elif self.__current_temp > self.__target_temp:
self.__current_temp = max(self.__target_temp, self.__current_temp - 1)

def _simulate_temperature_change(self, external_temp):
"""Protected method for subclasses to override"""
# Simulate external temperature influence
if external_temp < self.__current_temp:
self.__current_temp -= 0.1
elif external_temp > self.__current_temp:
self.__current_temp += 0.1

@classmethod
def get_device_count(cls):
return cls._device_count

def get_status(self):
return {
"room": self.room_name,
"current_temp": self.__current_temp,
"target_temp": self.__target_temp,
"online": self._is_online
}

# Usage
thermostat = SmartThermostat("Living Room", 22)
print(f"Current: {thermostat.current_temperature}°C") # 22°C
print(f"Target: {thermostat.target_temperature}°C") # 22°C

thermostat.target_temperature = 25
print(f"New target: {thermostat.target_temperature}°C") # 25°C
print(f"Current: {thermostat.current_temperature}°C") # 23°C (adjusted)

print(f"Total devices: {SmartThermostat.get_device_count()}") # 1

Challenges​

1. Banking System​

Create a BankAccount class with proper encapsulation for balance and account number.

💡 Show Answer
class BankAccount:
_next_account_number = 1000 # protected class variable

def __init__(self, owner_name, initial_balance=0):
self.owner_name = owner_name # public
self._account_number = BankAccount._next_account_number # protected
self.__balance = initial_balance # private
BankAccount._next_account_number += 1

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

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

def deposit(self, amount):
"""Deposit money to account"""
if not isinstance(amount, (int, float)):
raise TypeError("Amount must be a number")
if amount <= 0:
raise ValueError("Deposit amount must be positive")

self.__balance += amount
return self.__balance

def withdraw(self, amount):
"""Withdraw money from account"""
if not isinstance(amount, (int, float)):
raise TypeError("Amount must be a number")
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.__balance:
raise ValueError("Insufficient funds")

self.__balance -= amount
return self.__balance

def transfer(self, other_account, amount):
"""Transfer money to another account"""
if not isinstance(other_account, BankAccount):
raise TypeError("Target must be a BankAccount")

self.withdraw(amount) # This will validate the amount
other_account.deposit(amount)

def __str__(self):
return f"Account {self._account_number}: {self.owner_name} - ${self.__balance:.2f}"

# Usage
acc1 = BankAccount("Alice", 1000)
acc2 = BankAccount("Bob", 500)

print(acc1) # Account 1000: Alice - $1000.00
print(f"Account number: {acc1.account_number}") # 1000
print(f"Balance: ${acc1.balance}") # $1000

acc1.deposit(250)
print(f"After deposit: ${acc1.balance}") # $1250

acc1.withdraw(100)
print(f"After withdrawal: ${acc1.balance}") # $1150

acc1.transfer(acc2, 200)
print(f"Alice balance: ${acc1.balance}") # $950
print(f"Bob balance: ${acc2.balance}") # $700

2. User Authentication​

Create a User class with private password handling and proper validation.

💡 Show Answer
import hashlib
import re
from datetime import datetime, timedelta

class User:
_users_database = {} # protected class variable (simulated database)

def __init__(self, username, email, password):
self.username = username # public
self.email = email # public
self._created_at = datetime.now() # protected
self._last_login = None # protected
self.__password_hash = None # private
self.__login_attempts = 0 # private
self.__is_locked = False # private

self.password = password # Use setter for validation
User._users_database[username] = self

@property
def password(self):
"""Password is write-only"""
raise AttributeError("Password is not readable")

@password.setter
def password(self, password):
"""Set password with validation and hashing"""
self.__validate_password(password)
self.__password_hash = self.__hash_password(password)
self.__login_attempts = 0 # Reset attempts on password change
self.__is_locked = False

def __validate_password(self, password):
"""Private method to validate password strength"""
if len(password) < 8:
raise ValueError("Password must be at least 8 characters long")

if not re.search(r'[A-Z]', password):
raise ValueError("Password must contain at least one uppercase letter")

if not re.search(r'[a-z]', password):
raise ValueError("Password must contain at least one lowercase letter")

if not re.search(r'\d', password):
raise ValueError("Password must contain at least one digit")

if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
raise ValueError("Password must contain at least one special character")

def __hash_password(self, password):
"""Private method to hash password"""
return hashlib.sha256(password.encode()).hexdigest()

def authenticate(self, password):
"""Authenticate user with password"""
if self.__is_locked:
raise PermissionError("Account is locked due to too many failed attempts")

if self.__hash_password(password) == self.__password_hash:
self.__login_attempts = 0
self._last_login = datetime.now()
return True
else:
self.__login_attempts += 1
if self.__login_attempts >= 3:
self.__is_locked = True
raise PermissionError("Account locked after 3 failed attempts")
return False

@property
def is_locked(self):
"""Check if account is locked"""
return self.__is_locked

@property
def last_login(self):
"""Read-only access to last login time"""
return self._last_login

@property
def account_age(self):
"""Calculate account age in days"""
return (datetime.now() - self._created_at).days

def unlock_account(self, admin_password="admin123"):
"""Unlock account (admin function)"""
if admin_password == "admin123": # Simplified admin check
self.__is_locked = False
self.__login_attempts = 0
else:
raise PermissionError("Invalid admin password")

@classmethod
def get_user(cls, username):
"""Get user from database"""
return cls._users_database.get(username)

def __str__(self):
status = "🔒 LOCKED" if self.__is_locked else "✅ ACTIVE"
return f"User: {self.username} ({self.email}) - {status}"

# Usage
try:
user = User("alice", "alice@example.com", "SecurePass123!")
print(user) # User: alice (alice@example.com) - ✅ ACTIVE

# Successful authentication
if user.authenticate("SecurePass123!"):
print("Login successful!")

print(f"Last login: {user.last_login}")
print(f"Account age: {user.account_age} days")

# Failed authentication attempts
for i in range(3):
try:
user.authenticate("wrong_password")
except PermissionError as e:
print(f"Attempt {i+1}: {e}")
break
else:
print(f"Attempt {i+1}: Authentication failed")

print(f"Account locked: {user.is_locked}")

# Admin unlock
user.unlock_account("admin123")
print(f"After unlock: {user.is_locked}")

except ValueError as e:
print(f"Password validation error: {e}")

3. Shopping Cart​

Create a ShoppingCart class with private cart items and proper price calculations.

💡 Show Answer
from typing import Dict, List, Optional
from dataclasses import dataclass
from decimal import Decimal

@dataclass
class Product:
id: str
name: str
price: Decimal
category: str

def __post_init__(self):
if self.price < 0:
raise ValueError("Price cannot be negative")

class ShoppingCart:
_tax_rate = Decimal('0.08') # protected class variable (8% tax)

def __init__(self, customer_id: str):
self.customer_id = customer_id # public
self._cart_id = f"cart_{customer_id}_{hash(customer_id) % 10000}" # protected
self.__items: Dict[str, dict] = {} # private: {product_id: {product, quantity}}
self.__discount_percentage = Decimal('0') # private
self._created_at = None # protected

@property
def cart_id(self):
"""Read-only access to cart ID"""
return self._cart_id

@property
def item_count(self):
"""Total number of items in cart"""
return sum(item['quantity'] for item in self.__items.values())

@property
def unique_items(self):
"""Number of unique products in cart"""
return len(self.__items)

def add_item(self, product: Product, quantity: int = 1):
"""Add item to cart"""
if not isinstance(product, Product):
raise TypeError("Product must be a Product instance")
if not isinstance(quantity, int) or quantity <= 0:
raise ValueError("Quantity must be a positive integer")

if product.id in self.__items:
self.__items[product.id]['quantity'] += quantity
else:
self.__items[product.id] = {
'product': product,
'quantity': quantity
}

def remove_item(self, product_id: str, quantity: Optional[int] = None):
"""Remove item from cart"""
if product_id not in self.__items:
raise KeyError(f"Product {product_id} not in cart")

if quantity is None:
# Remove all of this item
del self.__items[product_id]
else:
current_qty = self.__items[product_id]['quantity']
if quantity >= current_qty:
del self.__items[product_id]
else:
self.__items[product_id]['quantity'] -= quantity

def update_quantity(self, product_id: str, new_quantity: int):
"""Update item quantity"""
if product_id not in self.__items:
raise KeyError(f"Product {product_id} not in cart")
if new_quantity <= 0:
self.remove_item(product_id)
else:
self.__items[product_id]['quantity'] = new_quantity

def clear_cart(self):
"""Remove all items from cart"""
self.__items.clear()
self.__discount_percentage = Decimal('0')

@property
def subtotal(self):
"""Calculate subtotal before tax and discount"""
return sum(
item['product'].price * item['quantity']
for item in self.__items.values()
)

@property
def discount_amount(self):
"""Calculate discount amount"""
return self.subtotal * (self.__discount_percentage / 100)

@property
def tax_amount(self):
"""Calculate tax amount"""
taxable_amount = self.subtotal - self.discount_amount
return taxable_amount * self._tax_rate

@property
def total(self):
"""Calculate final total"""
return self.subtotal - self.discount_amount + self.tax_amount

def apply_discount(self, percentage: float, promo_code: str = None):
"""Apply discount with optional promo code validation"""
if not isinstance(percentage, (int, float)):
raise TypeError("Discount percentage must be a number")
if not (0 <= percentage <= 100):
raise ValueError("Discount percentage must be between 0 and 100")

# Simple promo code validation
valid_codes = {"SAVE10": 10, "SAVE20": 20, "VIP": 25}
if promo_code:
if promo_code not in valid_codes:
raise ValueError(f"Invalid promo code: {promo_code}")
if percentage > valid_codes[promo_code]:
raise ValueError(f"Promo code {promo_code} only allows {valid_codes[promo_code]}% discount")

self.__discount_percentage = Decimal(str(percentage))

def get_items(self):
"""Get copy of cart items (read-only)"""
return [
{
'product_id': product_id,
'product_name': item['product'].name,
'price': item['product'].price,
'quantity': item['quantity'],
'subtotal': item['product'].price * item['quantity']
}
for product_id, item in self.__items.items()
]

def get_receipt(self):
"""Generate detailed receipt"""
receipt = []
receipt.append(f"Cart ID: {self._cart_id}")
receipt.append(f"Customer: {self.customer_id}")
receipt.append("-" * 40)

for item_info in self.get_items():
receipt.append(
f"{item_info['product_name']} x{item_info['quantity']} "
f"@ ${item_info['price']:.2f} = ${item_info['subtotal']:.2f}"
)

receipt.append("-" * 40)
receipt.append(f"Subtotal: ${self.subtotal:.2f}")

if self.__discount_percentage > 0:
receipt.append(f"Discount ({self.__discount_percentage}%): -${self.discount_amount:.2f}")

receipt.append(f"Tax ({self._tax_rate * 100}%): ${self.tax_amount:.2f}")
receipt.append(f"TOTAL: ${self.total:.2f}")

return "\n".join(receipt)

def __len__(self):
"""Support len() function"""
return self.item_count

def __str__(self):
return f"Cart {self._cart_id}: {self.item_count} items, Total: ${self.total:.2f}"

# Usage
# Create products
laptop = Product("P001", "Gaming Laptop", Decimal("1299.99"), "Electronics")
mouse = Product("P002", "Wireless Mouse", Decimal("29.99"), "Electronics")
keyboard = Product("P003", "Mechanical Keyboard", Decimal("89.99"), "Electronics")

# Create cart
cart = ShoppingCart("customer_123")
print(f"Cart ID: {cart.cart_id}")

# Add items
cart.add_item(laptop, 1)
cart.add_item(mouse, 2)
cart.add_item(keyboard, 1)

print(f"Items in cart: {len(cart)}")
print(f"Unique items: {cart.unique_items}")
print(f"Subtotal: ${cart.subtotal:.2f}")

# Apply discount
cart.apply_discount(10, "SAVE10")
print(f"After 10% discount: ${cart.total:.2f}")

# Print receipt
print("\n" + cart.get_receipt())

# Update quantities
cart.update_quantity("P002", 1) # Reduce mouse quantity to 1
print(f"\nAfter updating mouse quantity: ${cart.total:.2f}")

# Remove item
cart.remove_item("P003") # Remove keyboard
print(f"After removing keyboard: ${cart.total:.2f}")

4. Smart Lock System​

Create a SmartLock class with private access codes and security features.

💡 Show Answer
import hashlib
import time
from datetime import datetime, timedelta
from typing import List, Optional

class SmartLock:
_master_code = "123456" # protected class variable
_lockout_duration = 300 # 5 minutes in seconds

def __init__(self, lock_id: str, location: str):
self.lock_id = lock_id # public
self.location = location # public
self._is_locked = True # protected
self._battery_level = 100 # protected
self.__access_codes: List[str] = [] # private
self.__access_log: List[dict] = [] # private
self.__failed_attempts = 0 # private
self.__lockout_until: Optional[datetime] = None # private
self.__last_activity = datetime.now() # private

# Add default master code
self.__add_hashed_code(self._master_code, "Master Code", is_master=True)

def __hash_code(self, code: str) -> str:
"""Private method to hash access codes"""
return hashlib.sha256(code.encode()).hexdigest()

def __add_hashed_code(self, code: str, description: str = "User Code", is_master: bool = False):
"""Private method to add hashed code to system"""
hashed = self.__hash_code(code)
self.__access_codes.append({
'hash': hashed,
'description': description,
'created_at': datetime.now(),
'is_master': is_master,
'uses_remaining': None if is_master else 100 # Limited uses for non-master
})

def add_access_code(self, new_code: str, master_code: str, description: str = "User Code"):
"""Add new access code (requires master code)"""
if not self.__verify_master_access(master_code):
raise PermissionError("Invalid master code")

if len(new_code) < 4 or len(new_code) > 8:
raise ValueError("Access code must be 4-8 digits")

if not new_code.isdigit():
raise ValueError("Access code must contain only digits")

# Check if code already exists
new_hash = self.__hash_code(new_code)
for code_info in self.__access_codes:
if code_info['hash'] == new_hash:
raise ValueError("Access code already exists")

self.__add_hashed_code(new_code, description)
self.__log_activity("CODE_ADDED", f"New code added: {description}")

def remove_access_code(self, code_to_remove: str, master_code: str):
"""Remove access code (requires master code)"""
if not self.__verify_master_access(master_code):
raise PermissionError("Invalid master code")

remove_hash = self.__hash_code(code_to_remove)

for i, code_info in enumerate(self.__access_codes):
if code_info['hash'] == remove_hash and not code_info['is_master']:
removed_desc = code_info['description']
del self.__access_codes[i]
self.__log_activity("CODE_REMOVED", f"Code removed: {removed_desc}")
return

raise ValueError("Access code not found or cannot remove master code")

def __verify_master_access(self, master_code: str) -> bool:
"""Private method to verify master code"""
return master_code == self._master_code

def __is_locked_out(self) -> bool:
"""Private method to check if system is in lockout"""
if self.__lockout_until and datetime.now() < self.__lockout_until:
return True
elif self.__lockout_until and datetime.now() >= self.__lockout_until:
# Lockout period expired
self.__lockout_until = None
self.__failed_attempts = 0
return False

def __log_activity(self, action: str, details: str = ""):
"""Private method to log security events"""
self.__access_log.append({
'timestamp': datetime.now(),
'action': action,
'details': details,
'battery_level': self._battery_level
})
self.__last_activity = datetime.now()

# Simulate battery drain
self._battery_level = max(0, self._battery_level - 0.1)

def unlock(self, access_code: str) -> bool:
"""Unlock the device with access code"""
if self.__is_locked_out():
remaining = (self.__lockout_until - datetime.now()).seconds
raise PermissionError(f"System locked out. Try again in {remaining} seconds")

if self._battery_level <= 0:
raise RuntimeError("Battery depleted. Cannot operate lock")

code_hash = self.__hash_code(access_code)

# Check if code exists and is valid
for code_info in self.__access_codes:
if code_info['hash'] == code_hash:
# Check if non-master code has uses remaining
if not code_info['is_master'] and code_info['uses_remaining'] <= 0:
self.__log_activity("ACCESS_DENIED", f"Code expired: {code_info['description']}")
return False

# Successful unlock
self._is_locked = False
self.__failed_attempts = 0
self.__lockout_until = None

# Decrement uses for non-master codes
if not code_info['is_master']:
code_info['uses_remaining'] -= 1

self.__log_activity("UNLOCKED", f"Unlocked by: {code_info['description']}")
return True

# Failed attempt
self.__failed_attempts += 1
self.__log_activity("ACCESS_DENIED", f"Invalid code attempt #{self.__failed_attempts}")

if self.__failed_attempts >= 3:
self.__lockout_until = datetime.now() + timedelta(seconds=self._lockout_duration)
self.__log_activity("LOCKOUT", f"System locked for {self._lockout_duration} seconds")

return False

def lock(self) -> bool:
"""Lock the device"""
if self._battery_level <= 0:
raise RuntimeError("Battery depleted. Cannot operate lock")

self._is_locked = True
self.__log_activity("LOCKED", "Device manually locked")
return True

@property
def is_locked(self) -> bool:
"""Check if device is locked"""
return self._is_locked

@property
def battery_level(self) -> int:
"""Get battery level"""
return int(self._battery_level)

@property
def is_low_battery(self) -> bool:
"""Check if battery is low"""
return self._battery_level < 20

def get_status(self) -> dict:
"""Get comprehensive device status"""
return {
'lock_id': self.lock_id,
'location': self.location,
'is_locked': self._is_locked,
'battery_level': self.battery_level,
'is_low_battery': self.is_low_battery,
'is_locked_out': self.__is_locked_out(),
'active_codes': len([c for c in self.__access_codes if c.get('uses_remaining', 1) > 0]),
'last_activity': self.__last_activity,
'failed_attempts': self.__failed_attempts
}

def get_access_log(self, master_code: str, limit: int = 10) -> List[dict]:
"""Get access log (requires master code)"""
if not self.__verify_master_access(master_code):
raise PermissionError("Invalid master code")

return self.__access_log[-limit:]

def get_code_info(self, master_code: str) -> List[dict]:
"""Get information about access codes (requires master code)"""
if not self.__verify_master_access(master_code):
raise PermissionError("Invalid master code")

return [
{
'description': code['description'],
'created_at': code['created_at'],
'is_master': code['is_master'],
'uses_remaining': code['uses_remaining']
}
for code in self.__access_codes
]

def change_battery(self, master_code: str):
"""Change battery (requires master code)"""
if not self.__verify_master_access(master_code):
raise PermissionError("Invalid master code")

self._battery_level = 100
self.__log_activity("BATTERY_CHANGED", "Battery replaced")

def __str__(self):
status = "🔒 LOCKED" if self._is_locked else "🔓 UNLOCKED"
battery = f"🔋 {self.battery_level}%"
return f"SmartLock {self.lock_id} ({self.location}) - {status} {battery}"

# Usage
lock = SmartLock("SL001", "Front Door")
master = "123456"

print(lock) # SmartLock SL001 (Front Door) - 🔒 LOCKED 🔋 100%
print(f"Status: {lock.get_status()}")

# Add access codes
lock.add_access_code("1234", master, "Alice's Code")
lock.add_access_code("5678", master, "Bob's Code")
lock.add_access_code("9999", master, "Delivery Code")

# Show code information
print(f"\nAccess codes: {lock.get_code_info(master)}")

# Test unlock/lock
print(f"\nUnlock with Alice's code: {lock.unlock('1234')}")
print(f"Is locked: {lock.is_locked}")

print(f"Lock device: {lock.lock()}")
print(f"Is locked: {lock.is_locked}")

# Test failed attempts
print(f"\nTesting wrong codes...")
for i in range(4):
try:
result = lock.unlock("0000")
print(f"Attempt {i+1}: {result}")
except PermissionError as e:
print(f"Attempt {i+1}: {e}")
break

# View access log
print(f"\nAccess log:")
for entry in lock.get_access_log(master, 5):
print(f" {entry['timestamp'].strftime('%H:%M:%S')} - {entry['action']}: {entry['details']}")

# Test battery
print(f"\nBattery level: {lock.battery_level}%")
print(f"Low battery: {lock.is_low_battery}")