Skip to main content

Classes & Constructors

Introduction​

Python is an object-oriented language, and classes are a fundamental way to create objects. A class is a blueprint for creating objects that share the same structure (attributes) and behavior (methods).

When you create a new object from a class, the __init__ method (constructor) runs automatically. It sets up the object's initial state. Python uses the self parameter to refer to the current instance of the class.

Python also provides various ways to control access to attributes and methods, including conventions like single underscore (_) for "protected" and double underscore (__) for name mangling.


Concepts​

Declaring a Class

class Person:
def __init__(self, name, age):
self.name = name # public attribute
self.age = age

Creating Instances

alice = Person("Alice", 25)
print(alice.name) # "Alice"

Methods

class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def greet(self):
return f"Hi, I'm {self.name} and I'm {self.age} years old."

Private/Protected Attributes

class BankAccount:
def __init__(self, owner, starting_balance=0):
self.owner = owner # public
self._account_number = None # protected (convention)
self.__balance = starting_balance # private (name mangling)

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) # AttributeError: 'BankAccount' object has no attribute '__balance'

Properties (Getters & Setters)

Python's @property decorator provides elegant getter/setter syntax:

class Employee:
def __init__(self, name, salary):
self.name = name
self._salary = salary

@property
def salary(self):
return self._salary

@salary.setter
def salary(self, amount):
if amount < 0:
raise ValueError("Salary cannot be negative")
self._salary = amount

emp = Employee("Alice", 50000)
print(emp.salary) # 50000
emp.salary = 60000 # Uses setter

Class Variables vs Instance Variables

class BankAccount:
account_count = 0 # class variable (shared)

def __init__(self, owner, starting_balance=0):
self.owner = owner # instance variable
self.__balance = starting_balance
BankAccount.account_count += 1

@classmethod
def get_total_accounts(cls):
return cls.account_count

Challenges​

1. Person class​

  • Create a Person class with attributes name and age.
  • Make two instances and print their details.
💡 Show Answer
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def __str__(self):
return f"Person(name='{self.name}', age={self.age})"

def __repr__(self):
return f"Person('{self.name}', {self.age})"

# Create instances
person1 = Person("James", 38)
person2 = Person("Susan", 22)

print(person1) # Uses __str__
print(person2)
print(repr(person1)) # Uses __repr__

# Access attributes
print(f"Person 1: {person1.name}, {person1.age}")
print(f"Person 2: {person2.name}, {person2.age}")

# Convert to dictionary
print(vars(person1)) # {'name': 'James', 'age': 38}

2. Add introduce method​

  • Add a method introduce() to Person that returns "My name is [name] and I am [age] years old."
💡 Show Answer
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def introduce(self):
return f"My name is {self.name} and I am {self.age} years old."

def have_birthday(self):
"""Increment age by 1"""
self.age += 1
return f"Happy birthday! {self.name} is now {self.age}."

def is_adult(self):
"""Check if person is 18 or older"""
return self.age >= 18

# Test the class
person = Person("Alice", 17)
print(person.introduce())
print(f"Is adult: {person.is_adult()}")
print(person.have_birthday())
print(f"Is adult now: {person.is_adult()}")

3. Book with get_summary​

  • Create a Book class with attributes title, author, and year, and a method get_summary().
💡 Show Answer
class Book:
def __init__(self, title, author, year=None):
self.title = title
self.author = author
self.year = year
self.is_borrowed = False

def get_summary(self):
if self.year:
return f"'{self.title}' by {self.author} ({self.year})"
return f"'{self.title}' by {self.author}"

def borrow(self):
if self.is_borrowed:
return f"'{self.title}' is already borrowed."
self.is_borrowed = True
return f"You have borrowed '{self.title}'."

def return_book(self):
if not self.is_borrowed:
return f"'{self.title}' was not borrowed."
self.is_borrowed = False
return f"'{self.title}' has been returned."

def __str__(self):
status = "borrowed" if self.is_borrowed else "available"
return f"{self.get_summary()} - {status}"

# Test the class
book1 = Book("1984", "George Orwell", 1949)
book2 = Book("The Hobbit", "J.R.R. Tolkien")

print(book1.get_summary())
print(book2.get_summary())
print(book1.borrow())
print(book1)
print(book1.return_book())

4. Car with private attributes​

  • Create a Car class with public make/model and private mileage.
  • Add methods to drive and check mileage.
💡 Show Answer
class Car:
def __init__(self, make, model, year, mileage=0):
self.make = make
self.model = model
self.year = year
self.__mileage = mileage # private attribute
self._maintenance_due = False # protected attribute

def drive(self, miles):
if miles < 0:
raise ValueError("Miles cannot be negative")
self.__mileage += miles
if self.__mileage % 5000 == 0:
self._maintenance_due = True
return f"Drove {miles} miles. Total mileage: {self.__mileage}"

def get_mileage(self):
return self.__mileage

def needs_maintenance(self):
return self._maintenance_due

def perform_maintenance(self):
self._maintenance_due = False
return "Maintenance completed!"

def __str__(self):
return f"{self.year} {self.make} {self.model} ({self.__mileage} miles)"

# Test the class
car = Car("Toyota", "Corolla", 2020, 2000)
print(car)
print(car.drive(3500))
print(f"Needs maintenance: {car.needs_maintenance()}")
print(car.drive(1500))
print(f"Needs maintenance: {car.needs_maintenance()}")
print(car.perform_maintenance())

# Try to access private attribute (this will not work as expected)
try:
print(car.__mileage) # AttributeError
except AttributeError as e:
print(f"Cannot access private attribute: {e}")

# Python's name mangling allows access via _ClassName__attribute
print(f"Mileage via name mangling: {car._Car__mileage}")

5. Student with properties​

  • Create a Student class with a grade property using getter/setter.
💡 Show Answer
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self._grade = None
self._courses = []

@property
def grade(self):
return self._grade

@grade.setter
def grade(self, value):
if value is not None and not (0 <= value <= 100):
raise ValueError("Grade must be between 0 and 100")
self._grade = value

@property
def letter_grade(self):
if self._grade is None:
return "No grade assigned"
elif self._grade >= 90:
return "A"
elif self._grade >= 80:
return "B"
elif self._grade >= 70:
return "C"
elif self._grade >= 60:
return "D"
else:
return "F"

def add_course(self, course):
self._courses.append(course)

def get_courses(self):
return self._courses.copy() # Return copy to prevent external modification

def __str__(self):
return f"Student: {self.name} (ID: {self.student_id}), Grade: {self.grade} ({self.letter_grade})"

# Test the class
student = Student("Alice Johnson", "S12345")
student.add_course("Math")
student.add_course("Science")

try:
student.grade = 95
print(student)
print(f"Courses: {student.get_courses()}")

# Try invalid grade
student.grade = 150 # This will raise an error
except ValueError as e:
print(f"Error: {e}")

6. BankAccount with class methods​

  • Create a BankAccount class with a class variable to track total accounts.
💡 Show Answer
class BankAccount:
total_accounts = 0 # Class variable
bank_name = "Python Bank"

def __init__(self, owner, account_type="checking", initial_balance=0):
self.owner = owner
self.account_type = account_type
self.__balance = initial_balance
self.__transaction_history = []
BankAccount.total_accounts += 1
self.account_number = f"ACC{BankAccount.total_accounts:06d}"

def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.__balance += amount
self.__transaction_history.append(f"Deposited ${amount:.2f}")
return f"Deposited ${amount:.2f}. New balance: ${self.__balance:.2f}"

def withdraw(self, amount):
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.__balance:
raise ValueError("Insufficient funds")
self.__balance -= amount
self.__transaction_history.append(f"Withdrew ${amount:.2f}")
return f"Withdrew ${amount:.2f}. New balance: ${self.__balance:.2f}"

def get_balance(self):
return self.__balance

def get_transaction_history(self):
return self.__transaction_history.copy()

@classmethod
def get_total_accounts(cls):
return cls.total_accounts

@classmethod
def get_bank_name(cls):
return cls.bank_name

@staticmethod
def calculate_interest(principal, rate, time):
"""Calculate simple interest"""
return principal * rate * time

def __str__(self):
return (f"{self.bank_name} Account #{self.account_number}\n"
f"Owner: {self.owner}\n"
f"Type: {self.account_type}\n"
f"Balance: ${self.__balance:.2f}")

# Test the class
print(f"Bank: {BankAccount.get_bank_name()}")
print(f"Total accounts before: {BankAccount.get_total_accounts()}")

acc1 = BankAccount("Alice", "checking", 1000)
acc2 = BankAccount("Bob", "savings", 500)

print(f"Total accounts after: {BankAccount.get_total_accounts()}")
print(acc1)
print()
print(acc2)

print(acc1.deposit(200))
print(acc1.withdraw(50))
print(f"Transaction history: {acc1.get_transaction_history()}")

# Static method usage
interest = BankAccount.calculate_interest(1000, 0.05, 2)
print(f"Interest on $1000 at 5% for 2 years: ${interest}")

7. Rectangle with area and perimeter​

  • Create a Rectangle class with width and height, including area() and perimeter() methods.
💡 Show Answer
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height

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

def resize(self, width_factor, height_factor):
self.width *= width_factor
self.height *= height_factor

def __str__(self):
return f"Rectangle({self.width} x {self.height})"

def __eq__(self, other):
if not isinstance(other, Rectangle):
return False
return self.width == other.width and self.height == other.height

def __lt__(self, other):
return self.area() < other.area()

# Test the class
rect1 = Rectangle(5, 3)
rect2 = Rectangle(4, 4)

print(f"{rect1}: Area = {rect1.area()}, Perimeter = {rect1.perimeter()}")
print(f"{rect2}: Area = {rect2.area()}, Perimeter = {rect2.perimeter()}")
print(f"Is rect1 a square? {rect1.is_square()}")
print(f"Is rect2 a square? {rect2.is_square()}")

rect1.resize(2, 1.5)
print(f"After resize: {rect1}")

print(f"rect1 < rect2: {rect1 < rect2}")
print(f"rect1 == rect2: {rect1 == rect2}")

8. Temperature converter class​

  • Create a Temperature class that can convert between Celsius and Fahrenheit.
💡 Show Answer
class Temperature:
def __init__(self, value, unit="C"):
self.value = value
self.unit = unit.upper()
if self.unit not in ["C", "F"]:
raise ValueError("Unit must be 'C' for Celsius or 'F' for Fahrenheit")

@property
def celsius(self):
if self.unit == "C":
return self.value
return (self.value - 32) * 5/9

@property
def fahrenheit(self):
if self.unit == "F":
return self.value
return (self.value * 9/5) + 32

@property
def kelvin(self):
return self.celsius + 273.15

def convert_to(self, target_unit):
target_unit = target_unit.upper()
if target_unit == "C":
return Temperature(self.celsius, "C")
elif target_unit == "F":
return Temperature(self.fahrenheit, "F")
else:
raise ValueError("Target unit must be 'C' or 'F'")

@classmethod
def from_kelvin(cls, kelvin_value):
celsius_value = kelvin_value - 273.15
return cls(celsius_value, "C")

def __str__(self):
return f"{self.value:.1f}°{self.unit}"

def __eq__(self, other):
return abs(self.celsius - other.celsius) < 0.01

# Test the class
temp1 = Temperature(25, "C")
temp2 = Temperature(77, "F")

print(f"temp1: {temp1}")
print(f"temp1 in Fahrenheit: {temp1.fahrenheit:.1f}°F")
print(f"temp1 in Kelvin: {temp1.kelvin:.1f}K")

print(f"temp2: {temp2}")
print(f"temp2 in Celsius: {temp2.celsius:.1f}°C")

print(f"Are they equal? {temp1 == temp2}")

# Convert temperature
temp3 = temp1.convert_to("F")
print(f"temp1 converted to F: {temp3}")

# Create from Kelvin
temp4 = Temperature.from_kelvin(298.15)
print(f"From Kelvin: {temp4}")

9. Inheritance example​

  • Create a Vehicle base class and a Car subclass that inherits from it.
💡 Show Answer
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self._mileage = 0

def start(self):
return f"The {self.year} {self.make} {self.model} is starting..."

def stop(self):
return f"The {self.year} {self.make} {self.model} has stopped."

def drive(self, miles):
self._mileage += miles
return f"Drove {miles} miles. Total mileage: {self._mileage}"

def get_info(self):
return f"{self.year} {self.make} {self.model} ({self._mileage} miles)"

def __str__(self):
return self.get_info()

class Car(Vehicle):
def __init__(self, make, model, year, doors=4):
super().__init__(make, model, year) # Call parent constructor
self.doors = doors
self.is_convertible = False

def honk(self):
return "Beep beep!"

def get_info(self):
base_info = super().get_info()
return f"{base_info}, {self.doors} doors"

class Motorcycle(Vehicle):
def __init__(self, make, model, year, engine_size):
super().__init__(make, model, year)
self.engine_size = engine_size

def rev_engine(self):
return "Vroom vroom!"

def get_info(self):
base_info = super().get_info()
return f"{base_info}, {self.engine_size}cc engine"

# Test the classes
car = Car("Toyota", "Camry", 2020, 4)
motorcycle = Motorcycle("Honda", "CBR600", 2021, 600)

print("Car:")
print(car.start())
print(car.drive(100))
print(car.honk())
print(car.get_info())
print(car.stop())

print("\nMotorcycle:")
print(motorcycle.start())
print(motorcycle.drive(50))
print(motorcycle.rev_engine())
print(motorcycle.get_info())
print(motorcycle.stop())

# Check inheritance
print(f"\nIs car a Vehicle? {isinstance(car, Vehicle)}")
print(f"Is car a Car? {isinstance(car, Car)}")
print(f"Is motorcycle a Vehicle? {isinstance(motorcycle, Vehicle)}")

10. Library management system​

  • Create Book and Library classes to manage a collection of books.
💡 Show Answer
class Book:
def __init__(self, isbn, title, author, year, copies=1):
self.isbn = isbn
self.title = title
self.author = author
self.year = year
self.total_copies = copies
self.available_copies = copies
self.borrowed_by = []

def is_available(self):
return self.available_copies > 0

def borrow(self, borrower_name):
if not self.is_available():
return f"'{self.title}' is not available"
self.available_copies -= 1
self.borrowed_by.append(borrower_name)
return f"'{self.title}' borrowed by {borrower_name}"

def return_book(self, borrower_name):
if borrower_name not in self.borrowed_by:
return f"{borrower_name} did not borrow '{self.title}'"
self.borrowed_by.remove(borrower_name)
self.available_copies += 1
return f"'{self.title}' returned by {borrower_name}"

def __str__(self):
return f"'{self.title}' by {self.author} ({self.year}) - {self.available_copies}/{self.total_copies} available"

class Library:
def __init__(self, name):
self.name = name
self.books = {} # ISBN -> Book
self.members = set()

def add_book(self, book):
if book.isbn in self.books:
# Add copies to existing book
existing_book = self.books[book.isbn]
existing_book.total_copies += book.total_copies
existing_book.available_copies += book.available_copies
else:
self.books[book.isbn] = book
return f"Added '{book.title}' to {self.name}"

def remove_book(self, isbn):
if isbn in self.books:
book = self.books.pop(isbn)
return f"Removed '{book.title}' from {self.name}"
return "Book not found"

def register_member(self, member_name):
self.members.add(member_name)
return f"{member_name} registered as a member"

def borrow_book(self, isbn, member_name):
if member_name not in self.members:
return f"{member_name} is not a registered member"
if isbn not in self.books:
return "Book not found in library"
return self.books[isbn].borrow(member_name)

def return_book(self, isbn, member_name):
if isbn not in self.books:
return "Book not found in library"
return self.books[isbn].return_book(member_name)

def search_by_title(self, title):
results = []
for book in self.books.values():
if title.lower() in book.title.lower():
results.append(book)
return results

def search_by_author(self, author):
results = []
for book in self.books.values():
if author.lower() in book.author.lower():
results.append(book)
return results

def list_available_books(self):
return [book for book in self.books.values() if book.is_available()]

def get_statistics(self):
total_books = len(self.books)
total_copies = sum(book.total_copies for book in self.books.values())
available_copies = sum(book.available_copies for book in self.books.values())
borrowed_copies = total_copies - available_copies

return {
"total_books": total_books,
"total_copies": total_copies,
"available_copies": available_copies,
"borrowed_copies": borrowed_copies,
"members": len(self.members)
}

def __str__(self):
stats = self.get_statistics()
return (f"{self.name}\n"
f"Books: {stats['total_books']}, "
f"Copies: {stats['total_copies']}, "
f"Available: {stats['available_copies']}, "
f"Members: {stats['members']}")

# Test the library system
library = Library("Central Public Library")

# Add books
book1 = Book("978-0-452-28423-4", "1984", "George Orwell", 1949, 3)
book2 = Book("978-0-06-112008-4", "To Kill a Mockingbird", "Harper Lee", 1960, 2)
book3 = Book("978-0-7432-7356-5", "The Great Gatsby", "F. Scott Fitzgerald", 1925, 1)

print(library.add_book(book1))
print(library.add_book(book2))
print(library.add_book(book3))

# Register members
print(library.register_member("Alice Johnson"))
print(library.register_member("Bob Smith"))

print(f"\n{library}")

# Borrow books
print(f"\n{library.borrow_book('978-0-452-28423-4', 'Alice Johnson')}")
print(f"{library.borrow_book('978-0-452-28423-4', 'Bob Smith')}")

# Search books
print(f"\nBooks by Orwell: {[str(book) for book in library.search_by_author('Orwell')]}")

# List available books
print(f"\nAvailable books:")
for book in library.list_available_books():
print(f" {book}")

# Return book
print(f"\n{library.return_book('978-0-452-28423-4', 'Alice Johnson')}")

print(f"\nFinal statistics: {library.get_statistics()}")