Week 2 Tutorial: Encapsulation
Problem 1 (Easy): Temperature Reading
A weather station records temperatures but must ensure no reading goes below absolute zero (−273.15°C). Create a TemperatureReading class that stores a location and a temperature, using a property to validate the temperature.
- Define a class
TemperatureReadingwith__init__that acceptslocation(public) andtemperature. - Store
temperaturein a protected attribute_temperature. - Create a
@propertygetter fortemperaturethat returns_temperature. - Create a
@temperature.setterthat raises aValueErrorwith the message"Temperature cannot be below absolute zero"if the value is less than-273.15. Otherwise, store it. - Use
self.temperature = temperaturein__init__so the setter validates the initial value too.
Input
r1 = TemperatureReading("Tashkent", 35.0)
print(r1.location)
print(r1.temperature)
r1.temperature = -10.5
print(r1.temperature)
try:
r1.temperature = -300
except ValueError as e:
print(e)
Expected Output
Tashkent
35.0
-10.5
Temperature cannot be below absolute zero
Problem 2 (Easy+): Movie Rating
A movie review site needs to store movie information and control how ratings are assigned. Create a Movie class that validates its rating and provides a read-only property for the title.
- Define a class
Moviewith__init__that acceptstitleandrating. - Store
titlein a protected attribute_titleand expose it as a read-only property (getter only, no setter). - Create a
@propertygetter forratingthat returns_rating. - Create a
@rating.setterthat raises aValueErrorwith"Rating must be between 1 and 10"if the value is less than1or greater than10. Otherwise, store it. - Use
self.rating = ratingin__init__so the setter validates the initial value. - Define a method
display()that prints:<title> - <rating>/10
Input
m = Movie("Inception", 9)
m.display()
m.rating = 7
m.display()
try:
m.rating = 11
except ValueError as e:
print(e)
try:
m.title = "New Title"
except AttributeError:
print("Cannot change title")
Expected Output
Inception - 9/10
Inception - 7/10
Rating must be between 1 and 10
Cannot change title
Problem 3 (Medium): Product Inventory
An online store needs a Product class to manage items in its catalog. Each product has a name, a price, and a quantity in stock.
- The product’s
nameshould not be changeable after creation. - The
pricemust always be a positive number. - The
quantitycannot be negative. - A method
restock(amount)adds the given amount to the current quantity. The amount must be positive. - A method
sell(amount)reduces the quantity by the given amount. The amount must be positive and cannot exceed the current quantity. - A method
total_value()returns the product’s price multiplied by its quantity.
Input
p = Product("Laptop", 999.99, 10)
print(p.name)
print(p.price)
print(p.quantity)
print(p.total_value())
p.price = 899.99
p.restock(5)
print(p.quantity)
print(p.total_value())
p.sell(3)
print(p.quantity)
try:
p.price = -50
except ValueError as e:
print(e)
try:
p.sell(100)
except ValueError as e:
print(e)
try:
p.name = "Tablet"
except AttributeError:
print("Cannot change product name")
Expected Output
Laptop
999.99
10
9999.9
899.99
15
13499.85
12
Price must be positive
Insufficient stock
Cannot change product name
Problem 4 (Medium+): User Account
A platform needs a UserAccount class to manage user credentials securely.
- Each account has a
username, anemail, and a password. - The
usernameshould not be changeable after creation. - The
emailmust contain exactly one"@"symbol. Invalid emails should raiseValueErrorwith"Invalid email address". - The password must be stored as a private attribute (using name mangling). It should not be readable or writable through a property.
- A method
change_password(old_password, new_password)updates the password only ifold_passwordmatches the current one. If it doesn’t match, raiseValueErrorwith"Incorrect password". The new password must be at least 6 characters long — otherwise raiseValueErrorwith"Password must be at least 6 characters". - A method
login(password)returnsTrueif the password matches,Falseotherwise. - A method
display_info()returns a string in the format:<username> (<email>)
Input
u = UserAccount("alisher", "alisher@mail.com", "secret123")
print(u.username)
print(u.email)
print(u.display_info())
print(u.login("wrong"))
print(u.login("secret123"))
u.email = "new@email.com"
print(u.display_info())
u.change_password("secret123", "newpass456")
print(u.login("secret123"))
print(u.login("newpass456"))
try:
u.change_password("wrong", "abc123")
except ValueError as e:
print(e)
try:
u.change_password("newpass456", "short")
except ValueError as e:
print(e)
try:
u.email = "invalid-email"
except ValueError as e:
print(e)
try:
u.username = "hacker"
except AttributeError:
print("Cannot change username")
try:
print(u.__password)
except AttributeError:
print("Cannot access private password")
Expected Output
alisher
alisher@mail.com
alisher (alisher@mail.com)
False
True
alisher (new@email.com)
False
True
Incorrect password
Password must be at least 6 characters
Invalid email address
Cannot change username
Cannot access private password
Problem 5 (Advanced): Shopping Cart
An e-commerce platform needs a ShoppingCart class to manage a customer’s selected items, calculate totals, and handle discount codes.
- Each cart is created with a
customer_nameand adiscount_code. The customer name should not be changeable after creation. The discount code must be stored as a private attribute (name mangling) — it should not be accessible from outside the class. - Items are stored internally. Each item has a name, a unit price, and a quantity.
- A method
add_item(name, price, quantity)adds an item to the cart. Price must be positive and quantity must be a positive integer. If an item with the same name already exists, only its quantity should increase (the price stays the same). - A method
remove_item(name)removes an item entirely. RaiseValueErrorwith"Item not found"if the item doesn’t exist. - A read-only property
item_countreturns the total number of individual items (sum of all quantities). - A read-only property
subtotalreturns the sum ofprice × quantityfor all items, rounded to 2 decimal places. - A method
apply_discount(code)checks the code against the stored private discount code. If it matches, a 10% discount is activated. If the code is wrong, raiseValueErrorwith"Invalid discount code". If a discount has already been applied, raiseValueErrorwith"Discount already applied". - A read-only property
totalreturns the subtotal with the discount applied (if any), rounded to 2 decimal places. - A method
summary()returns a string:<customer_name>: <item_count> items, total: $<total> - Add type hints to all method parameters and return types. Add a docstring to the class.
Input
cart = ShoppingCart("Nodira", "SAVE10")
cart.add_item("Notebook", 12.99, 3)
cart.add_item("Pen", 1.50, 10)
cart.add_item("Notebook", 12.99, 2)
print(cart.customer_name)
print(cart.item_count)
print(cart.subtotal)
print(cart.total)
print(cart.summary())
cart.apply_discount("SAVE10")
print(cart.total)
print(cart.summary())
cart.remove_item("Pen")
print(cart.item_count)
print(cart.total)
try:
cart.add_item("Eraser", -5, 1)
except ValueError as e:
print(e)
try:
cart.remove_item("Marker")
except ValueError as e:
print(e)
try:
cart.apply_discount("SAVE10")
except ValueError as e:
print(e)
try:
cart.apply_discount("WRONG")
except ValueError as e:
print(e)
try:
cart.customer_name = "Someone"
except AttributeError:
print("Cannot change customer name")
try:
print(cart.__discount_code)
except AttributeError:
print("Cannot access private discount code")
Expected Output
Nodira
15
79.95
79.95
Nodira: 15 items, total: $79.95
71.95
Nodira: 15 items, total: $71.95
5
58.46 (or 58.45)
Price must be positive
Item not found
Discount already applied
Discount already applied
Cannot change customer name
Cannot access private discount code
Problem 6 (Advanced): Bank Account
A banking application needs a BankAccount class to manage customer funds with transaction logging and PIN-based security.
- Each account is created with an
ownername, an initialbalance, and apin(a string). The owner should not be changeable after creation. The pin must be stored as a private attribute (name mangling) — it should not be accessible from outside the class. - The
balancemust be exposed as a read-only property. It should not be settable directly. The balance must never be negative — if an invalid initial balance is given, raiseValueErrorwith"Balance cannot be negative". - Every successful deposit or withdrawal is recorded internally as a transaction tuple:
(type, amount, resulting_balance)where type is"deposit"or"withdrawal". The transaction list should not be accessible from outside. - A method
deposit(amount, pin)adds money to the account. The pin must match — otherwise raiseValueErrorwith"Invalid PIN". The amount must be positive — otherwise raiseValueErrorwith"Amount must be positive". - A method
withdraw(amount, pin)removes money. Same validations as deposit, plus if the amount exceeds the balance, raiseValueErrorwith"Insufficient funds". - A read-only computed property
transaction_countreturns the total number of transactions recorded. - A method
get_statement(pin)verifies the pin and returns a multi-line string listing all transactions, one per line in the format:<type>: $<amount> | Balance: $<resulting_balance>. If no transactions exist, return"No transactions yet". - A method
transfer(amount, pin, other_account)withdraws from this account and deposits into the other account. The pin is only required for this account (deposit into the other account does not require the other account’s pin — call the internal logic directly). If the withdrawal fails, the deposit should not happen. - Add type hints to all method parameters and return types. Add a docstring to the class.
Input
a1 = BankAccount("Dilnoza", 1000.0, "1234")
a2 = BankAccount("Jasur", 500.0, "5678")
print(a1.owner)
print(a1.balance)
print(a1.transaction_count)
a1.deposit(250.0, "1234")
print(a1.balance)
print(a1.transaction_count)
a1.withdraw(100.0, "1234")
print(a1.balance)
print(a1.get_statement("1234"))
a1.transfer(200.0, "1234", a2)
print(a1.balance)
print(a2.balance)
print(a1.transaction_count)
print(a2.transaction_count)
try:
a1.deposit(50.0, "wrong")
except ValueError as e:
print(e)
try:
a1.withdraw(5000.0, "1234")
except ValueError as e:
print(e)
try:
a1.withdraw(-10.0, "1234")
except ValueError as e:
print(e)
try:
a1.balance = 999999
except AttributeError:
print("Cannot set balance directly")
try:
a1.owner = "Hacker"
except AttributeError:
print("Cannot change owner")
try:
print(a1.__pin)
except AttributeError:
print("Cannot access private PIN")
try:
print(a1.get_statement("wrong"))
except ValueError as e:
print(e)
Expected Output
Dilnoza
1000.0
0
1250.0
1
1150.0
deposit: $250.0 | Balance: $1250.0
withdrawal: $100.0 | Balance: $1150.0
950.0
700.0
3
1
Invalid PIN
Insufficient funds
Amount must be positive
Cannot set balance directly
Cannot change owner
Cannot access private PIN
Invalid PIN
This content will be available starting February 17, 2026.