Python Classes and Objects

A Python class is a blueprint or template that defines the structure and behavior of objects. Think of a Python class as a cookie cutter that shapes cookies - the class defines what the cookie will look like, while the actual cookies are the objects created from that class.

An object in Python is an instance of a class. When you create an object from a Python class, you’re instantiating that class. Each object has its own set of attributes (data) and methods (functions) defined by the class.

# Simple class definition
class Car:
    pass  # Empty class body

# Creating objects from the class
my_car = Car()  # my_car is an object of class Car
your_car = Car()  # your_car is another object of class Car

Python Class Definition Syntax

Python classes are defined using the class keyword followed by the class name. The class name should follow PascalCase convention (first letter of each word capitalized).

class StudentRecord:
    """A class to represent student information"""
    pass

Python Class Attributes

Python class attributes are variables that belong to a class. There are two types of attributes in Python classes:

Instance Attributes

Instance attributes are specific to each object created from the Python class. Each object maintains its own copy of instance attributes.

class Smartphone:
    def __init__(self, brand, model, price):
        self.brand = brand      # Instance attribute
        self.model = model      # Instance attribute
        self.price = price      # Instance attribute

Class Attributes

Class attributes are shared by all objects of the Python class. They belong to the class itself rather than individual objects.

class BankAccount:
    bank_name = "Python Bank"    # Class attribute
    interest_rate = 0.03         # Class attribute
    
    def __init__(self, account_holder, balance):
        self.account_holder = account_holder  # Instance attribute
        self.balance = balance                # Instance attribute

The init Method in Python Classes

The __init__ method is a special method called a constructor in Python classes. This method is automatically called when you create a new object from the class. The __init__ method initializes the object’s attributes.

class GameCharacter:
    def __init__(self, name, health, attack_power):
        self.name = name                    # Initialize instance attribute
        self.health = health                # Initialize instance attribute
        self.attack_power = attack_power    # Initialize instance attribute
        self.level = 1                      # Default attribute value

# Creating objects using __init__
hero = GameCharacter("Warrior", 100, 25)
villain = GameCharacter("Dragon", 200, 40)

Python Class Methods

Methods are functions defined inside Python classes that operate on objects. There are three types of methods in Python classes:

Instance Methods

Instance methods are the most common type of methods in Python classes. They work with instance attributes and can access both instance and class attributes.

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def calculate_area(self):           # Instance method
        return self.width * self.height
    
    def calculate_perimeter(self):      # Instance method
        return 2 * (self.width + self.height)
    
    def display_info(self):             # Instance method
        print(f"Rectangle: {self.width}x{self.height}")
        print(f"Area: {self.calculate_area()}")
        print(f"Perimeter: {self.calculate_perimeter()}")

Class Methods

Class methods work with class attributes and are decorated with @classmethod. They receive the class as the first argument (conventionally named cls).

class Employee:
    company_name = "TechCorp"           # Class attribute
    total_employees = 0                 # Class attribute
    
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
        Employee.total_employees += 1   # Increment class attribute
    
    @classmethod
    def get_company_info(cls):          # Class method
        return f"Company: {cls.company_name}, Employees: {cls.total_employees}"
    
    @classmethod
    def create_intern(cls, name):       # Class method as alternative constructor
        return cls(name, 30000)         # Create object with default salary

Static Methods

Static methods don’t access instance or class attributes. They’re decorated with @staticmethod and behave like regular functions but belong to the class namespace.

class MathOperations:
    @staticmethod
    def add_numbers(a, b):              # Static method
        return a + b
    
    @staticmethod
    def multiply_numbers(a, b):         # Static method
        return a * b
    
    @staticmethod
    def is_even(number):                # Static method
        return number % 2 == 0

Python Object Creation and Usage

Creating objects from Python classes is straightforward. You call the class name like a function, passing any required arguments to the __init__ method.

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
        self.is_borrowed = False
    
    def borrow_book(self):
        if not self.is_borrowed:
            self.is_borrowed = True
            return f"You borrowed '{self.title}'"
        return f"'{self.title}' is already borrowed"
    
    def return_book(self):
        if self.is_borrowed:
            self.is_borrowed = False
            return f"You returned '{self.title}'"
        return f"'{self.title}' was not borrowed"

# Creating and using objects
book1 = Book("Python Programming", "John Doe", 350)
book2 = Book("Data Structures", "Jane Smith", 420)

print(book1.borrow_book())  # Calling method on object
print(book2.title)          # Accessing attribute

Accessing Python Class Attributes and Methods

Python classes allow you to access attributes and methods using dot notation. You can access both instance and class attributes from objects.

class VideoGame:
    genre = "Action"                    # Class attribute
    
    def __init__(self, title, developer, release_year):
        self.title = title              # Instance attribute
        self.developer = developer      # Instance attribute
        self.release_year = release_year # Instance attribute
    
    def game_info(self):
        return f"{self.title} by {self.developer} ({self.release_year})"

# Creating object and accessing attributes
game = VideoGame("Adventure Quest", "GameStudio", 2023)

# Accessing instance attributes
print(game.title)           # Adventure Quest
print(game.developer)       # GameStudio

# Accessing class attribute
print(game.genre)           # Action
print(VideoGame.genre)      # Action (accessing via class)

# Calling methods
print(game.game_info())     # Adventure Quest by GameStudio (2023)

Python Class Inheritance

Python classes support inheritance, allowing you to create new classes based on existing ones. The new class (child/subclass) inherits attributes and methods from the parent class (superclass).

class Animal:                           # Parent class
    def __init__(self, name, species):
        self.name = name
        self.species = species
    
    def make_sound(self):
        return f"{self.name} makes a sound"
    
    def move(self):
        return f"{self.name} moves"

class Dog(Animal):                      # Child class inheriting from Animal
    def __init__(self, name, breed):
        super().__init__(name, "Canine")  # Call parent __init__
        self.breed = breed
    
    def make_sound(self):               # Override parent method
        return f"{self.name} barks"
    
    def fetch(self):                    # New method specific to Dog
        return f"{self.name} fetches the ball"

class Cat(Animal):                      # Another child class
    def __init__(self, name, color):
        super().__init__(name, "Feline")
        self.color = color
    
    def make_sound(self):               # Override parent method
        return f"{self.name} meows"
    
    def climb(self):                    # New method specific to Cat
        return f"{self.name} climbs the tree"

Python Class Encapsulation

Encapsulation in Python classes involves controlling access to attributes and methods. Python uses naming conventions to indicate privacy levels:

class BankAccount:
    def __init__(self, account_number, initial_balance):
        self.account_number = account_number    # Public attribute
        self._balance = initial_balance         # Protected attribute (convention)
        self.__pin = 1234                      # Private attribute (name mangling)
    
    def deposit(self, amount):                 # Public method
        if amount > 0:
            self._balance += amount
            return f"Deposited ${amount}. New balance: ${self._balance}"
        return "Invalid deposit amount"
    
    def _validate_transaction(self, amount):   # Protected method
        return amount > 0 and amount <= self._balance
    
    def __encrypt_data(self, data):            # Private method
        return f"encrypted_{data}"
    
    def withdraw(self, amount, pin):
        if pin == self.__pin and self._validate_transaction(amount):
            self._balance -= amount
            return f"Withdrew ${amount}. New balance: ${self._balance}"
        return "Invalid withdrawal"
    
    def get_balance(self):                     # Public method to access private data
        return self._balance

Python Class Polymorphism

Polymorphism allows objects of different Python classes to be treated as objects of a common base class. This enables writing flexible and reusable code.

class Shape:                                # Base class
    def __init__(self, name):
        self.name = name
    
    def area(self):                         # Method to be overridden
        raise NotImplementedError("Subclass must implement area method")
    
    def perimeter(self):                    # Method to be overridden
        raise NotImplementedError("Subclass must implement perimeter method")

class Circle(Shape):                        # Derived class
    def __init__(self, radius):
        super().__init__("Circle")
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14159 * self.radius

class Square(Shape):                        # Derived class
    def __init__(self, side):
        super().__init__("Square")
        self.side = side
    
    def area(self):
        return self.side ** 2
    
    def perimeter(self):
        return 4 * self.side

# Polymorphism in action
def print_shape_info(shape):                # Function accepts any Shape object
    print(f"{shape.name}:")
    print(f"Area: {shape.area()}")
    print(f"Perimeter: {shape.perimeter()}")

Special Methods in Python Classes

Python classes can implement special methods (also called magic methods or dunder methods) to customize object behavior:

class Product:
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity
    
    def __str__(self):                      # String representation for users
        return f"{self.name} - ${self.price} (Qty: {self.quantity})"
    
    def __repr__(self):                     # String representation for developers
        return f"Product('{self.name}', {self.price}, {self.quantity})"
    
    def __eq__(self, other):                # Equality comparison
        if isinstance(other, Product):
            return self.name == other.name and self.price == other.price
        return False
    
    def __lt__(self, other):                # Less than comparison
        if isinstance(other, Product):
            return self.price < other.price
        return NotImplemented
    
    def __add__(self, other):               # Addition operator
        if isinstance(other, Product):
            if self.name == other.name:
                return Product(self.name, self.price, self.quantity + other.quantity)
        return NotImplemented
    
    def __len__(self):                      # Length operator
        return self.quantity

Complete Example: Library Management System

Here’s a comprehensive example that demonstrates Python classes and objects in a real-world scenario:

from datetime import datetime, timedelta

class Book:
    """Represents a book in the library"""
    total_books = 0                         # Class attribute
    
    def __init__(self, title, author, isbn, copies=1):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.copies = copies
        self.available_copies = copies
        self.borrowed_by = []               # List of members who borrowed this book
        Book.total_books += 1
    
    def __str__(self):
        return f"'{self.title}' by {self.author}"
    
    def __repr__(self):
        return f"Book('{self.title}', '{self.author}', '{self.isbn}', {self.copies})"
    
    def is_available(self):
        """Check if book is available for borrowing"""
        return self.available_copies > 0
    
    def borrow(self, member):
        """Borrow a book to a member"""
        if self.is_available():
            self.available_copies -= 1
            self.borrowed_by.append(member)
            return True
        return False
    
    def return_book(self, member):
        """Return a book from a member"""
        if member in self.borrowed_by:
            self.available_copies += 1
            self.borrowed_by.remove(member)
            return True
        return False
    
    @classmethod
    def get_total_books(cls):
        """Get total number of books in the system"""
        return cls.total_books

class Member:
    """Represents a library member"""
    
    def __init__(self, name, member_id, email):
        self.name = name
        self.member_id = member_id
        self.email = email
        self.borrowed_books = []            # List of borrowed books
        self.join_date = datetime.now()
    
    def __str__(self):
        return f"Member: {self.name} (ID: {self.member_id})"
    
    def __repr__(self):
        return f"Member('{self.name}', '{self.member_id}', '{self.email}')"
    
    def borrow_book(self, book):
        """Borrow a book"""
        if len(self.borrowed_books) < 5:    # Maximum 5 books per member
            if book.borrow(self):
                self.borrowed_books.append(book)
                return f"Successfully borrowed {book}"
            return f"'{book.title}' is not available"
        return "Maximum borrowing limit reached (5 books)"
    
    def return_book(self, book):
        """Return a borrowed book"""
        if book in self.borrowed_books:
            if book.return_book(self):
                self.borrowed_books.remove(book)
                return f"Successfully returned {book}"
        return f"You haven't borrowed '{book.title}'"
    
    def get_borrowed_books(self):
        """Get list of borrowed books"""
        return [str(book) for book in self.borrowed_books]

class Library:
    """Represents the library system"""
    
    def __init__(self, name, address):
        self.name = name
        self.address = address
        self.books = []                     # List of all books
        self.members = []                   # List of all members
        self.created_date = datetime.now()
    
    def __str__(self):
        return f"{self.name} Library"
    
    def add_book(self, book):
        """Add a book to the library"""
        self.books.append(book)
        return f"Added {book} to the library"
    
    def add_member(self, member):
        """Add a member to the library"""
        self.members.append(member)
        return f"Added {member} to the library"
    
    def find_book(self, title):
        """Find a book by title"""
        for book in self.books:
            if book.title.lower() == title.lower():
                return book
        return None
    
    def find_member(self, member_id):
        """Find a member by ID"""
        for member in self.members:
            if member.member_id == member_id:
                return member
        return None
    
    def get_available_books(self):
        """Get list of available books"""
        return [book for book in self.books if book.is_available()]
    
    def get_library_stats(self):
        """Get library statistics"""
        total_books = len(self.books)
        available_books = len(self.get_available_books())
        total_members = len(self.members)
        
        return {
            'total_books': total_books,
            'available_books': available_books,
            'borrowed_books': total_books - available_books,
            'total_members': total_members
        }

# Example usage and testing
if __name__ == "__main__":
    # Create library
    city_library = Library("City Central Library", "123 Main Street")
    
    # Create books
    book1 = Book("Python Programming", "John Smith", "978-1234567890", 3)
    book2 = Book("Data Structures", "Jane Doe", "978-1234567891", 2)
    book3 = Book("Web Development", "Bob Johnson", "978-1234567892", 1)
    
    # Add books to library
    print(city_library.add_book(book1))
    print(city_library.add_book(book2))
    print(city_library.add_book(book3))
    
    # Create members
    member1 = Member("Alice Brown", "M001", "alice@email.com")
    member2 = Member("Charlie Davis", "M002", "charlie@email.com")
    
    # Add members to library
    print(city_library.add_member(member1))
    print(city_library.add_member(member2))
    
    # Borrow books
    print(member1.borrow_book(book1))
    print(member1.borrow_book(book2))
    print(member2.borrow_book(book1))
    
    # Display member's borrowed books
    print(f"Alice's borrowed books: {member1.get_borrowed_books()}")
    print(f"Charlie's borrowed books: {member2.get_borrowed_books()}")
    
    # Display library statistics
    stats = city_library.get_library_stats()
    print(f"\nLibrary Statistics:")
    print(f"Total books: {stats['total_books']}")
    print(f"Available books: {stats['available_books']}")
    print(f"Borrowed books: {stats['borrowed_books']}")
    print(f"Total members: {stats['total_members']}")
    
    # Return a book
    print(f"\n{member1.return_book(book1)}")
    
    # Check updated statistics
    stats = city_library.get_library_stats()
    print(f"\nUpdated Statistics:")
    print(f"Available books: {stats['available_books']}")
    print(f"Borrowed books: {stats['borrowed_books']}")
    
    # Display total books using class method
    print(f"\nTotal books in system: {Book.get_total_books()}")

Expected Output:

Added 'Python Programming' by John Smith to the library
Added 'Data Structures' by Jane Doe to the library
Added 'Web Development' by Bob Johnson to the library
Added Member: Alice Brown (ID: M001) to the library
Added Member: Charlie Davis (ID: M002) to the library
Successfully borrowed 'Python Programming' by John Smith
Successfully borrowed 'Data Structures' by Jane Doe
Successfully borrowed 'Python Programming' by John Smith
Alice's borrowed books: ["'Python Programming' by John Smith", "'Data Structures' by Jane Doe"]
Charlie's borrowed books: ["'Python Programming' by John Smith"]

Library Statistics:
Total books: 3
Available books: 1
Borrowed books: 2
Total members: 2

Successfully returned 'Python Programming' by John Smith

Updated Statistics:
Available books: 2
Borrowed books: 1

Total books in system: 3

This comprehensive example demonstrates Python classes and objects working together in a practical library management system, showcasing inheritance, encapsulation, method types, and real-world object interactions. The code includes proper error handling, data validation, and demonstrates how Python classes and objects can model complex real-world systems effectively.