🔧 Lesson 6.2: Properties, Methods, and Constructors
In the previous lesson you built your first classes with attributes and basic methods. Now it's time to level up: methods that return values, controlling who can access your data, string representations so your objects print nicely, and static methods that belong to the class itself rather than any single object.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Write methods that return values instead of just printing
- Define string representations so objects display nicely
- Control access with public / private visibility
- Use properties (getters and setters) to protect data
- Create static methods that belong to the class, not instances
- Build methods that support chaining
Estimated Time: 60 minutes
Project: Build a Product class for a shopping cart system
📑 In This Lesson
Methods That Return Values
In Lesson 16, most of our methods used print to show results. That's fine for demos, but real-world methods usually return values so other code can use them.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
# ❌ Printing — other code can't use this result
def show_area(self):
print(self.width * self.height)
# ✅ Returning — the caller decides what to do with it
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def is_square(self):
return self.width == self.height
r = Rectangle(5, 3)
# Now we can USE the result
a = r.area() # 15
p = r.perimeter() # 16
print(f"Area: {a}, Perimeter: {p}")
# Use in expressions
total = r.area() + Rectangle(4, 4).area()
print(f"Combined area: {total}") # 31
# Use in conditions
if r.is_square():
print("It's a square!")
else:
print("It's a rectangle!") # ← this one
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
// ❌ Printing — other code can't use this result
showArea() {
console.log(this.width * this.height);
}
// ✅ Returning — the caller decides what to do with it
area() {
return this.width * this.height;
}
perimeter() {
return 2 * (this.width + this.height);
}
isSquare() {
return this.width === this.height;
}
}
let r = new Rectangle(5, 3);
// Now we can USE the result
let a = r.area(); // 15
let p = r.perimeter(); // 16
console.log(`Area: ${a}, Perimeter: ${p}`);
// Use in expressions
let total = r.area() + new Rectangle(4, 4).area();
console.log(`Combined area: ${total}`); // 31
// Use in conditions
if (r.isSquare()) {
console.log("It's a square!");
} else {
console.log("It's a rectangle!"); // ← this one
}
class Rectangle
{
public double Width;
public double Height;
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
// Methods with RETURN TYPES (not void)
public double Area()
{
return Width * Height;
}
public double Perimeter()
{
return 2 * (Width + Height);
}
public bool IsSquare()
{
return Width == Height;
}
}
Rectangle r = new Rectangle(5, 3);
// Now we can USE the result
double a = r.Area(); // 15
double p = r.Perimeter(); // 16
Console.WriteLine($"Area: {a}, Perimeter: {p}");
// Use in expressions
double total = r.Area() + new Rectangle(4, 4).Area();
Console.WriteLine($"Combined area: {total}"); // 31
// Use in conditions
if (r.IsSquare())
Console.WriteLine("It's a square!");
else
Console.WriteLine("It's a rectangle!"); // ← this one
⚠️ C# Requires Return Types
In C#, every method must declare its return type: void (returns nothing), int, double, bool, string, etc. Python and JavaScript don't require this — they can return anything or nothing. This is part of C#'s static typing philosophy: the compiler checks your types before the program even runs.
💡 Rule of Thumb
Return when other code needs the result. Print only for debugging or user-facing output methods. A method named area() should return the number; a method named display() can print. Most methods should return.
🎓 Instructor Note: Delivery Guidance
This is a critical mindset shift for beginners. Many students (especially from Lesson 16) default to putting print() in every method. Show a concrete scenario: "What if you need to add two rectangles' areas together? If area() prints instead of returning, you can't do r1.area() + r2.area() — you get None + None." The lightbulb moment usually happens when they see return values being used in if statements and arithmetic.
String Representations
When you print an object, you get something ugly like <__main__.Player object at 0x7f...>. Every language lets you define how your object should display as a string.
class Player:
def __init__(self, name, health=100):
self.name = name
self.health = health
self.inventory = []
# __str__ — for print() and str()
def __str__(self):
return f"{self.name} (HP: {self.health})"
# __repr__ — for debugging (shows in lists, console)
def __repr__(self):
return f"Player('{self.name}', {self.health})"
hero = Player("Alice", 85)
# __str__ is used by print()
print(hero) # Alice (HP: 85)
# __repr__ is used in lists and debugger
party = [Player("Alice", 85), Player("Bob", 100)]
print(party) # [Player('Alice', 85), Player('Bob', 100)]
# str() and repr() call these directly
print(str(hero)) # Alice (HP: 85)
print(repr(hero)) # Player('Alice', 85)
class Player {
constructor(name, health = 100) {
this.name = name;
this.health = health;
this.inventory = [];
}
// toString() — used by template literals and String()
toString() {
return `${this.name} (HP: ${this.health})`;
}
}
let hero = new Player("Alice", 85);
// toString() is called automatically in string contexts
console.log(`Hero: ${hero}`); // Hero: Alice (HP: 85)
console.log(String(hero)); // Alice (HP: 85)
// ⚠️ console.log(hero) still shows the raw object
console.log(hero);
// Player { name: 'Alice', health: 85, inventory: [] }
// But concatenation calls toString()
console.log("Status: " + hero); // Status: Alice (HP: 85)
class Player
{
public string Name;
public int Health;
public List<string> Inventory = new();
public Player(string name, int health = 100)
{
Name = name;
Health = health;
}
// ToString() — override the built-in method
public override string ToString()
{
return $"{Name} (HP: {Health})";
}
}
Player hero = new Player("Alice", 85);
// ToString() is called automatically
Console.WriteLine(hero); // Alice (HP: 85)
Console.WriteLine($"Hero: {hero}"); // Hero: Alice (HP: 85)
// Works in collections too
var party = new List<Player>
{
new Player("Alice", 85), new Player("Bob", 100)
};
Console.WriteLine(string.Join(", ", party));
// Alice (HP: 85), Bob (HP: 100)
String Representation Quick Reference
| Purpose | 🐍 Python | ⚡ JavaScript | 🔷 C# |
|---|---|---|---|
| User-friendly display | __str__ |
toString() |
override ToString() |
| Debug representation | __repr__ |
No built-in equivalent | No built-in equivalent |
| Keyword | None (special name) | None (special name) | override |
💡 Python's Two Methods
Python is unique in having both __str__ and __repr__. The rule: __str__ is for humans ("Alice with 85 HP"), __repr__ is for developers (ideally something you could paste back into code to recreate the object). When in doubt, define __repr__ first — Python falls back to it if __str__ isn't defined.
Public vs. Private
Should outside code be able to directly change player.health = -999? Probably not. Access control lets you decide which attributes and methods are accessible from outside the class.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner # Public — anyone can read
self._balance = balance # "Private" by convention (single _)
self.__pin = "1234" # Name-mangled (double __)
def deposit(self, amount):
if amount > 0:
self._balance += amount
def get_balance(self):
return self._balance
acct = BankAccount("Alice", 1000)
# Public — works fine
print(acct.owner) # Alice
# "Private" — works but signals "don't touch this"
print(acct._balance) # 1000 (Python trusts you)
# Name-mangled — harder to access (but not impossible)
# print(acct.__pin) # AttributeError!
print(acct._BankAccount__pin) # "1234" (Python's "mangling")
# The Pythonic way: use the method
print(acct.get_balance()) # 1000
class BankAccount {
#balance; // Private field (# prefix)
#pin;
constructor(owner, balance = 0) {
this.owner = owner; // Public
this.#balance = balance; // Private
this.#pin = "1234"; // Private
}
deposit(amount) {
if (amount > 0) this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
let acct = new BankAccount("Alice", 1000);
// Public — works fine
console.log(acct.owner); // Alice
// Private — actually enforced!
// console.log(acct.#balance); // SyntaxError!
// Must use the public method
console.log(acct.getBalance()); // 1000
class BankAccount
{
public string Owner; // Public — anyone can access
private decimal _balance; // Private — only this class
private string _pin; // Private
public BankAccount(string owner, decimal balance = 0)
{
Owner = owner;
_balance = balance;
_pin = "1234";
}
public void Deposit(decimal amount)
{
if (amount > 0) _balance += amount;
}
public decimal GetBalance()
{
return _balance;
}
}
BankAccount acct = new BankAccount("Alice", 1000);
// Public — works fine
Console.WriteLine(acct.Owner); // Alice
// Private — compiler error!
// Console.WriteLine(acct._balance); // Error: inaccessible
// Must use the public method
Console.WriteLine(acct.GetBalance()); // 1000
Access Control Comparison
| Feature | 🐍 Python | ⚡ JavaScript | 🔷 C# |
|---|---|---|---|
| Public | Default (no prefix) | Default (no prefix) | public keyword |
| Private | _name (convention)__name (mangled) |
#name (enforced) |
private keyword |
| Enforcement | Honor system 🤝 | Hard enforced 🔒 | Hard enforced 🔒 |
| Philosophy | "We're all consenting adults" | Strict privacy | Strict privacy |
💡 Why Make Things Private?
Private attributes let you control how data changes. Instead of anyone writing acct.balance = -1000000, they must use acct.deposit() or acct.withdraw() — methods that can validate the input. This is called encapsulation: hiding the internal details and exposing a controlled interface.
🎓 Instructor Note: Delivery Guidance
Python's approach often surprises students coming from other languages. Emphasize that the single underscore _balance is the community standard — it signals "internal, don't touch" but Python won't stop you. The double underscore __name does name-mangling to make accidental access harder, but it's still accessible if you try. JavaScript's # fields are relatively new (ES2022) and truly private. C# has the most traditional access control with explicit keywords.
Properties: Getters and Setters
Sometimes you want attribute-like syntax (player.health) but with method-like control (validation, computed values). That's what properties do — they look like attributes but run code behind the scenes.
class Player:
def __init__(self, name, health=100):
self.name = name
self._health = health # Internal storage
@property
def health(self):
"""Getter — runs when you READ player.health"""
return self._health
@health.setter
def health(self, value):
"""Setter — runs when you WRITE player.health = X"""
if value < 0:
self._health = 0
elif value > 100:
self._health = 100
else:
self._health = value
@property
def is_alive(self):
"""Read-only property — no setter defined"""
return self._health > 0
hero = Player("Alice")
# Looks like an attribute, but runs the getter/setter
hero.health = 150 # Setter clamps to 100
print(hero.health) # 100
hero.health = -50 # Setter clamps to 0
print(hero.health) # 0
print(hero.is_alive) # False (read-only property)
# hero.is_alive = True # AttributeError — no setter!
class Player {
#health;
constructor(name, health = 100) {
this.name = name;
this.#health = health;
}
// Getter — runs when you READ player.health
get health() {
return this.#health;
}
// Setter — runs when you WRITE player.health = X
set health(value) {
if (value < 0) this.#health = 0;
else if (value > 100) this.#health = 100;
else this.#health = value;
}
// Read-only property — getter only
get isAlive() {
return this.#health > 0;
}
}
let hero = new Player("Alice");
// Looks like a property, but runs the getter/setter
hero.health = 150; // Setter clamps to 100
console.log(hero.health); // 100
hero.health = -50; // Setter clamps to 0
console.log(hero.health); // 0
console.log(hero.isAlive); // false (read-only)
// hero.isAlive = true; // Silently ignored (no setter)
class Player
{
public string Name;
private int _health;
public Player(string name, int health = 100)
{
Name = name;
Health = health; // Uses the property setter!
}
// Property with getter and setter
public int Health
{
get { return _health; }
set
{
if (value < 0) _health = 0;
else if (value > 100) _health = 100;
else _health = value;
}
}
// Read-only property (getter only)
public bool IsAlive => _health > 0;
}
Player hero = new Player("Alice");
// Looks like a field, but runs the getter/setter
hero.Health = 150; // Setter clamps to 100
Console.WriteLine(hero.Health); // 100
hero.Health = -50; // Setter clamps to 0
Console.WriteLine(hero.Health); // 0
Console.WriteLine(hero.IsAlive); // False (read-only)
// hero.IsAlive = true; // Compile error!
Clamp to 100"] B --> C["_health = 100"] D["print(hero.health)"] -->|"Getter runs"| E["Return _health"] E --> F["Output: 100"] style A fill:#ef4444,stroke:#dc2626,color:#fff style D fill:#22c55e,stroke:#16a34a,color:#fff style C fill:#3b82f6,stroke:#2563eb,color:#fff style F fill:#3b82f6,stroke:#2563eb,color:#fff
✅ Best of Both Worlds
Properties give you the simple syntax of direct attribute access (hero.health = 50) with the safety of method validation. The caller doesn't need to know or care that a getter/setter is running behind the scenes.
Static Methods
Regular methods operate on a specific object (hero.take_damage()). Static methods belong to the class itself — they don't need an instance. They're useful for utility functions, factory methods, and operations that relate to the class but not to any particular object.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9/5 + 32
def __str__(self):
return f"{self.celsius}°C ({self.fahrenheit:.1f}°F)"
# Static method — no self, no instance needed
@staticmethod
def from_fahrenheit(f):
"""Factory method: create a Temperature from °F"""
return Temperature((f - 32) * 5/9)
@staticmethod
def boiling_point():
return Temperature(100)
@staticmethod
def freezing_point():
return Temperature(0)
# Call static methods on the CLASS, not an instance
water_boils = Temperature.boiling_point()
print(water_boils) # 100°C (212.0°F)
body_temp = Temperature.from_fahrenheit(98.6)
print(body_temp) # 37.0°C (98.6°F)
# Regular usage still works
today = Temperature(28)
print(today) # 28°C (82.4°F)
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return this.celsius * 9/5 + 32;
}
toString() {
return `${this.celsius}°C (${this.fahrenheit.toFixed(1)}°F)`;
}
// Static methods — called on the class
static fromFahrenheit(f) {
return new Temperature((f - 32) * 5/9);
}
static boilingPoint() {
return new Temperature(100);
}
static freezingPoint() {
return new Temperature(0);
}
}
// Call static methods on the CLASS
let waterBoils = Temperature.boilingPoint();
console.log(`${waterBoils}`); // 100°C (212.0°F)
let bodyTemp = Temperature.fromFahrenheit(98.6);
console.log(`${bodyTemp}`); // 37.0°C (98.6°F)
// Regular usage
let today = new Temperature(28);
console.log(`${today}`); // 28°C (82.4°F)
class Temperature
{
public double Celsius;
public Temperature(double celsius)
{
Celsius = celsius;
}
public double Fahrenheit => Celsius * 9.0 / 5 + 32;
public override string ToString()
{
return $"{Celsius}°C ({Fahrenheit:F1}°F)";
}
// Static methods — called on the class
public static Temperature FromFahrenheit(double f)
{
return new Temperature((f - 32) * 5.0 / 9);
}
public static Temperature BoilingPoint()
{
return new Temperature(100);
}
public static Temperature FreezingPoint()
{
return new Temperature(0);
}
}
// Call static methods on the CLASS
Temperature waterBoils = Temperature.BoilingPoint();
Console.WriteLine(waterBoils); // 100°C (212.0°F)
Temperature bodyTemp = Temperature.FromFahrenheit(98.6);
Console.WriteLine(bodyTemp); // 37.0°C (98.6°F)
Temperature today = new Temperature(28);
Console.WriteLine(today); // 28°C (82.4°F)
When to Use Static Methods
| Use Case | Example | Why Static? |
|---|---|---|
| Factory methods | Temperature.from_fahrenheit(98.6) |
Alternative way to create instances |
| Well-known constants | Temperature.boiling_point() |
Predefined values that don't vary per instance |
| Utility functions | MathHelper.clamp(value, min, max) |
Pure calculation, no instance state needed |
| Validation | Email.is_valid("test@example.com") |
Check data before creating an instance |
🎓 Instructor Note: Delivery Guidance
The key distinction: regular methods need an object (hero.heal()), static methods need the class (Temperature.from_fahrenheit()). A good way to decide: "Does this method need to access self/this?" If no → consider making it static. The factory pattern (from_fahrenheit) is especially useful — students will encounter it in real codebases. Python also has @classmethod which receives cls instead of self, but for beginners, @staticmethod is sufficient.
Method Chaining
If a method returns self / this, you can call another method on the result — creating a fluent chain of operations.
class QueryBuilder:
def __init__(self, table):
self.table = table
self._conditions = []
self._order = None
self._limit = None
def where(self, condition):
self._conditions.append(condition)
return self # ← Return self for chaining!
def order_by(self, field):
self._order = field
return self
def limit(self, n):
self._limit = n
return self
def build(self):
query = f"SELECT * FROM {self.table}"
if self._conditions:
query += " WHERE " + " AND ".join(self._conditions)
if self._order:
query += f" ORDER BY {self._order}"
if self._limit:
query += f" LIMIT {self._limit}"
return query
# Without chaining — verbose
q = QueryBuilder("users")
q.where("age > 18")
q.where("active = true")
q.order_by("name")
q.limit(10)
print(q.build())
# With chaining — clean and readable!
query = (QueryBuilder("users")
.where("age > 18")
.where("active = true")
.order_by("name")
.limit(10)
.build())
print(query)
# SELECT * FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10
class QueryBuilder {
constructor(table) {
this.table = table;
this._conditions = [];
this._order = null;
this._limit = null;
}
where(condition) {
this._conditions.push(condition);
return this; // ← Return this for chaining!
}
orderBy(field) {
this._order = field;
return this;
}
limit(n) {
this._limit = n;
return this;
}
build() {
let query = `SELECT * FROM ${this.table}`;
if (this._conditions.length)
query += ` WHERE ${this._conditions.join(" AND ")}`;
if (this._order) query += ` ORDER BY ${this._order}`;
if (this._limit) query += ` LIMIT ${this._limit}`;
return query;
}
}
// Chained — clean and readable!
let query = new QueryBuilder("users")
.where("age > 18")
.where("active = true")
.orderBy("name")
.limit(10)
.build();
console.log(query);
// SELECT * FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10
class QueryBuilder
{
private string _table;
private List<string> _conditions = new();
private string? _order;
private int? _limit;
public QueryBuilder(string table) { _table = table; }
public QueryBuilder Where(string condition)
{
_conditions.Add(condition);
return this; // ← Return this for chaining!
}
public QueryBuilder OrderBy(string field)
{
_order = field;
return this;
}
public QueryBuilder Limit(int n)
{
_limit = n;
return this;
}
public string Build()
{
string query = $"SELECT * FROM {_table}";
if (_conditions.Count > 0)
query += $" WHERE {string.Join(" AND ", _conditions)}";
if (_order != null) query += $" ORDER BY {_order}";
if (_limit != null) query += $" LIMIT {_limit}";
return query;
}
}
// Chained!
string query = new QueryBuilder("users")
.Where("age > 18")
.Where("active = true")
.OrderBy("name")
.Limit(10)
.Build();
Console.WriteLine(query);
💡 The Secret: return self / return this
Method chaining works because each method returns the object itself. So .where("age > 18") returns the same QueryBuilder, and you can immediately call .order_by("name") on it. You've already seen this pattern — jQuery, LINQ, and many libraries use it.
Putting It All Together
Let's build a complete Product class that uses everything from this lesson: return values, string representations, private data, properties, and a static factory method.
class Product:
_tax_rate = 0.08 # Class-level constant
def __init__(self, name, price, quantity=0):
self.name = name
self._price = 0 # Will be set via property
self.price = price # Use property setter for validation
self._quantity = 0
self.quantity = quantity
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price cannot be negative")
self._price = round(value, 2)
@property
def quantity(self):
return self._quantity
@quantity.setter
def quantity(self, value):
if value < 0:
raise ValueError("Quantity cannot be negative")
self._quantity = value
@property
def subtotal(self):
return round(self._price * self._quantity, 2)
@property
def total_with_tax(self):
return round(self.subtotal * (1 + Product._tax_rate), 2)
@property
def in_stock(self):
return self._quantity > 0
def __str__(self):
stock = "In Stock" if self.in_stock else "Out of Stock"
return f"{self.name}: ${self.price:.2f} ({stock})"
def __repr__(self):
return f"Product('{self.name}', {self.price}, {self.quantity})"
@staticmethod
def from_dict(data):
return Product(data["name"], data["price"], data.get("quantity", 0))
# Usage
laptop = Product("Laptop", 999.99, 5)
print(laptop) # Laptop: $999.99 (In Stock)
print(f"Subtotal: ${laptop.subtotal}") # $4999.95
print(f"With tax: ${laptop.total_with_tax}") # $5399.95
# From dictionary (static factory)
data = {"name": "Mouse", "price": 29.99, "quantity": 50}
mouse = Product.from_dict(data)
print(mouse) # Mouse: $29.99 (In Stock)
# Property validation
# laptop.price = -100 # ValueError: Price cannot be negative
class Product {
static #taxRate = 0.08;
#price;
#quantity;
constructor(name, price, quantity = 0) {
this.name = name;
this.#price = 0;
this.price = price; // Use setter
this.#quantity = 0;
this.quantity = quantity;
}
get price() { return this.#price; }
set price(value) {
if (value < 0) throw new Error("Price cannot be negative");
this.#price = Math.round(value * 100) / 100;
}
get quantity() { return this.#quantity; }
set quantity(value) {
if (value < 0) throw new Error("Quantity cannot be negative");
this.#quantity = value;
}
get subtotal() {
return Math.round(this.#price * this.#quantity * 100) / 100;
}
get totalWithTax() {
return Math.round(this.subtotal * (1 + Product.#taxRate) * 100) / 100;
}
get inStock() { return this.#quantity > 0; }
toString() {
let stock = this.inStock ? "In Stock" : "Out of Stock";
return `${this.name}: $${this.price.toFixed(2)} (${stock})`;
}
static fromObject(data) {
return new Product(data.name, data.price, data.quantity ?? 0);
}
}
let laptop = new Product("Laptop", 999.99, 5);
console.log(`${laptop}`);
console.log(`Subtotal: $${laptop.subtotal}`);
console.log(`With tax: $${laptop.totalWithTax}`);
let mouse = Product.fromObject({ name: "Mouse", price: 29.99, quantity: 50 });
console.log(`${mouse}`);
class Product
{
private static readonly double TaxRate = 0.08;
public string Name;
private double _price;
private int _quantity;
public Product(string name, double price, int quantity = 0)
{
Name = name;
Price = price; // Use property setter
Quantity = quantity;
}
public double Price
{
get => _price;
set
{
if (value < 0) throw new ArgumentException("Price cannot be negative");
_price = Math.Round(value, 2);
}
}
public int Quantity
{
get => _quantity;
set
{
if (value < 0) throw new ArgumentException("Quantity cannot be negative");
_quantity = value;
}
}
public double Subtotal => Math.Round(_price * _quantity, 2);
public double TotalWithTax => Math.Round(Subtotal * (1 + TaxRate), 2);
public bool InStock => _quantity > 0;
public override string ToString()
{
string stock = InStock ? "In Stock" : "Out of Stock";
return $"{Name}: ${Price:F2} ({stock})";
}
public static Product FromDictionary(Dictionary<string, object> data)
{
string name = (string)data["name"];
double price = Convert.ToDouble(data["price"]);
int qty = data.ContainsKey("quantity") ? Convert.ToInt32(data["quantity"]) : 0;
return new Product(name, price, qty);
}
}
Product laptop = new Product("Laptop", 999.99, 5);
Console.WriteLine(laptop);
Console.WriteLine($"Subtotal: ${laptop.Subtotal}");
Console.WriteLine($"With tax: ${laptop.TotalWithTax}");
Exercises
🏋️ Exercise 1: Task Manager
Objective: Create a Task class with:
- Constructor:
title(string), optionalpriority(1–5, default 3) - Private
completedstatus (boolean, startsfalse) - Property for
prioritythat validates 1–5 range complete()method that marks the task done__str__/toStringthat shows:[✓] Buy groceries (Priority: 2)or[ ] Buy groceries (Priority: 2)- Static method
from_string("Buy groceries|2")that parses a pipe-separated string
✅ Solution
class Task:
def __init__(self, title, priority=3):
self.title = title
self._priority = 3
self.priority = priority # Use setter
self._completed = False
@property
def priority(self):
return self._priority
@priority.setter
def priority(self, value):
if not 1 <= value <= 5:
raise ValueError("Priority must be 1-5")
self._priority = value
@property
def completed(self):
return self._completed
def complete(self):
self._completed = True
def __str__(self):
check = "✓" if self._completed else " "
return f"[{check}] {self.title} (Priority: {self._priority})"
def __repr__(self):
return f"Task('{self.title}', {self._priority})"
@staticmethod
def from_string(s):
parts = s.split("|")
title = parts[0].strip()
priority = int(parts[1].strip()) if len(parts) > 1 else 3
return Task(title, priority)
# Test
tasks = [
Task("Buy groceries", 2),
Task("Write report", 5),
Task.from_string("Call dentist|4"),
Task.from_string("Read book")
]
tasks[0].complete()
for task in tasks:
print(task)
# [✓] Buy groceries (Priority: 2)
# [ ] Write report (Priority: 5)
# [ ] Call dentist (Priority: 4)
# [ ] Read book (Priority: 3)
class Task {
#priority;
#completed = false;
constructor(title, priority = 3) {
this.title = title;
this.#priority = 3;
this.priority = priority;
}
get priority() { return this.#priority; }
set priority(value) {
if (value < 1 || value > 5) throw new Error("Priority must be 1-5");
this.#priority = value;
}
get completed() { return this.#completed; }
complete() { this.#completed = true; }
toString() {
let check = this.#completed ? "✓" : " ";
return `[${check}] ${this.title} (Priority: ${this.#priority})`;
}
static fromString(s) {
let parts = s.split("|");
let title = parts[0].trim();
let priority = parts.length > 1 ? parseInt(parts[1].trim()) : 3;
return new Task(title, priority);
}
}
let tasks = [
new Task("Buy groceries", 2),
new Task("Write report", 5),
Task.fromString("Call dentist|4"),
Task.fromString("Read book")
];
tasks[0].complete();
tasks.forEach(t => console.log(`${t}`));
class Task
{
public string Title;
private int _priority;
private bool _completed = false;
public Task(string title, int priority = 3)
{
Title = title;
Priority = priority;
}
public int Priority
{
get => _priority;
set
{
if (value < 1 || value > 5)
throw new ArgumentException("Priority must be 1-5");
_priority = value;
}
}
public bool Completed => _completed;
public void Complete() => _completed = true;
public override string ToString()
{
string check = _completed ? "✓" : " ";
return $"[{check}] {Title} (Priority: {_priority})";
}
public static Task FromString(string s)
{
var parts = s.Split("|");
string title = parts[0].Trim();
int priority = parts.Length > 1 ? int.Parse(parts[1].Trim()) : 3;
return new Task(title, priority);
}
}
var tasks = new List<Task>
{
new Task("Buy groceries", 2),
new Task("Write report", 5),
Task.FromString("Call dentist|4"),
Task.FromString("Read book")
};
tasks[0].Complete();
tasks.ForEach(t => Console.WriteLine(t));
🏋️ Exercise 2: Shopping Cart
Objective: Build a ShoppingCart class that holds Product objects:
- Constructor takes an
ownername add(product)— adds a Product to the cart, returnsself/thisfor chainingremove(product_name)— removes a product by nametotal(property) — returns the sum of all product subtotalsitem_count(property) — number of items__str__/toString— shows cart summary
✅ Solution
class Product:
def __init__(self, name, price, quantity=1):
self.name = name
self.price = price
self.quantity = quantity
@property
def subtotal(self):
return round(self.price * self.quantity, 2)
def __str__(self):
return f"{self.name} x{self.quantity} = ${self.subtotal:.2f}"
class ShoppingCart:
def __init__(self, owner):
self.owner = owner
self._items = []
def add(self, product):
self._items.append(product)
return self # Chaining!
def remove(self, product_name):
self._items = [p for p in self._items if p.name != product_name]
return self
@property
def total(self):
return round(sum(p.subtotal for p in self._items), 2)
@property
def item_count(self):
return len(self._items)
def __str__(self):
lines = [f"🛒 {self.owner}'s Cart ({self.item_count} items):"]
for item in self._items:
lines.append(f" • {item}")
lines.append(f" Total: ${self.total:.2f}")
return "\n".join(lines)
# Test with chaining
cart = (ShoppingCart("Alice")
.add(Product("Laptop", 999.99))
.add(Product("Mouse", 29.99, 2))
.add(Product("USB Cable", 9.99, 3)))
print(cart)
# 🛒 Alice's Cart (3 items):
# • Laptop x1 = $999.99
# • Mouse x2 = $59.98
# • USB Cable x3 = $29.97
# Total: $1089.94
cart.remove("Mouse")
print(f"\nAfter removing Mouse: ${cart.total:.2f}") # $1029.96
class Product {
constructor(name, price, quantity = 1) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
get subtotal() {
return Math.round(this.price * this.quantity * 100) / 100;
}
toString() {
return `${this.name} x${this.quantity} = $${this.subtotal.toFixed(2)}`;
}
}
class ShoppingCart {
#items = [];
constructor(owner) {
this.owner = owner;
}
add(product) {
this.#items.push(product);
return this;
}
remove(productName) {
this.#items = this.#items.filter(p => p.name !== productName);
return this;
}
get total() {
return Math.round(
this.#items.reduce((sum, p) => sum + p.subtotal, 0) * 100
) / 100;
}
get itemCount() { return this.#items.length; }
toString() {
let lines = [`🛒 ${this.owner}'s Cart (${this.itemCount} items):`];
this.#items.forEach(item => lines.push(` • ${item}`));
lines.push(` Total: $${this.total.toFixed(2)}`);
return lines.join("\n");
}
}
// Test with chaining
let cart = new ShoppingCart("Alice")
.add(new Product("Laptop", 999.99))
.add(new Product("Mouse", 29.99, 2))
.add(new Product("USB Cable", 9.99, 3));
console.log(`${cart}`);
cart.remove("Mouse");
console.log(`\nAfter removing Mouse: $${cart.total.toFixed(2)}`);
class Product
{
public string Name;
public double Price;
public int Quantity;
public Product(string name, double price, int quantity = 1)
{ Name = name; Price = price; Quantity = quantity; }
public double Subtotal => Math.Round(Price * Quantity, 2);
public override string ToString() =>
$"{Name} x{Quantity} = ${Subtotal:F2}";
}
class ShoppingCart
{
public string Owner;
private List<Product> _items = new();
public ShoppingCart(string owner) { Owner = owner; }
public ShoppingCart Add(Product product)
{ _items.Add(product); return this; }
public ShoppingCart Remove(string productName)
{ _items.RemoveAll(p => p.Name == productName); return this; }
public double Total => Math.Round(_items.Sum(p => p.Subtotal), 2);
public int ItemCount => _items.Count;
public override string ToString()
{
var lines = new List<string>
{ $"🛒 {Owner}'s Cart ({ItemCount} items):" };
_items.ForEach(item => lines.Add($" • {item}"));
lines.Add($" Total: ${Total:F2}");
return string.Join("\n", lines);
}
}
var cart = new ShoppingCart("Alice")
.Add(new Product("Laptop", 999.99))
.Add(new Product("Mouse", 29.99, 2))
.Add(new Product("USB Cable", 9.99, 3));
Console.WriteLine(cart);
cart.Remove("Mouse");
Console.WriteLine($"\nAfter removing Mouse: ${cart.Total:F2}");
🎓 Instructor Note: Delivery Guidance
Exercise 1 (Task) covers properties, validation, string representation, and static factory methods in one focused class. Exercise 2 (ShoppingCart) is the capstone — it uses composition (a cart containing products), method chaining, computed properties, and ties back to collections. Challenge fast students: add a discount(percent) method to ShoppingCart, or make the cart prevent duplicate products by updating quantity instead of adding a new entry.
Summary
🎉 Key Takeaways
- Return values: Methods should usually
returnresults, notprintthem — let the caller decide what to do - String representation: Python
__str__/__repr__, JStoString(), C#override ToString() - Public vs. Private: Python uses convention (
_name), JS uses#name, C# usesprivatekeyword - Properties give attribute syntax with method control: Python
@property, JSget/set, C# property syntax - Static methods belong to the class, not instances: Python
@staticmethod, JS/C#static - Method chaining works by returning
self/thisfrom methods - Encapsulation = hiding data + exposing a controlled interface
🚀 What's Next?
You've built complete, well-structured classes with controlled access and useful methods. In the next lesson, we tackle the most powerful OOP concept: inheritance and polymorphism — creating new classes that build on existing ones, sharing behavior while adding specialization.
🎯 Quick Check
Question 1: In Python, what decorator creates a property with a getter?
Question 2: How do you declare a private field in JavaScript?
Question 3: What must a method return to support chaining?