Python Classes/Objects

Deep dive into Python classes and objects with advanced concepts and practical examples.

Advanced Class Concepts

Building on the basic OOP concepts, let's explore more advanced features of Python classes.

Class Methods and Static Methods

Instance Methods

Regular methods that operate on instance data:

Example

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def introduce(self):  # Instance method
        return f"Hi, I'm {self.name} and I'm {self.age} years old"
    
    def have_birthday(self):  # Instance method
        self.age += 1
        return f"Happy birthday! Now I'm {self.age}"

person = Person("Alice", 25)
print(person.introduce())
print(person.have_birthday())

Class Methods

Methods that operate on the class itself, not instances:

Example

class Person:
    species = "Homo sapiens"
    population = 0
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.population += 1
    
    @classmethod
    def get_species(cls):
        return cls.species
    
    @classmethod
    def get_population(cls):
        return cls.population
    
    @classmethod
    def from_string(cls, person_str):
        name, age = person_str.split('-')
        return cls(name, int(age))

# Using class methods
print(Person.get_species())  # Homo sapiens
person1 = Person("Alice", 25)
person2 = Person.from_string("Bob-30")
print(Person.get_population())  # 2

Static Methods

Methods that don't operate on instance or class data:

Example

class MathUtils:
    @staticmethod
    def add(x, y):
        return x + y
    
    @staticmethod
    def is_even(number):
        return number % 2 == 0
    
    @staticmethod
    def factorial(n):
        if n <= 1:
            return 1
        return n * MathUtils.factorial(n - 1)

# Using static methods
print(MathUtils.add(5, 3))      # 8
print(MathUtils.is_even(4))     # True
print(MathUtils.factorial(5))   # 120

Special Methods (Magic Methods)

Special methods allow you to define how objects behave with built-in operations:

Example - Comprehensive special methods:

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    
    def __str__(self):
        return f"{self.title} by {self.author}"
    
    def __repr__(self):
        return f"Book('{self.title}', '{self.author}', {self.pages})"
    
    def __len__(self):
        return self.pages
    
    def __eq__(self, other):
        if isinstance(other, Book):
            return self.title == other.title and self.author == other.author
        return False
    
    def __lt__(self, other):
        return self.pages < other.pages
    
    def __add__(self, other):
        if isinstance(other, Book):
            return self.pages + other.pages
        return self.pages + other

# Usage
book1 = Book("1984", "George Orwell", 328)
book2 = Book("Animal Farm", "George Orwell", 112)

print(str(book1))           # 1984 by George Orwell
print(repr(book1))          # Book('1984', 'George Orwell', 328)
print(len(book1))           # 328
print(book1 == book2)       # False
print(book1 > book2)        # True
print(book1 + book2)        # 440

Property Decorators

Properties allow you to access methods like attributes:

Example - Circle class with properties:

import math

class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value
    
    @property
    def area(self):
        return math.pi * self._radius ** 2
    
    @property
    def circumference(self):
        return 2 * math.pi * self._radius
    
    @property
    def diameter(self):
        return 2 * self._radius

# Usage
circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Area: {circle.area:.2f}")
print(f"Circumference: {circle.circumference:.2f}")

circle.radius = 10
print(f"New area: {circle.area:.2f}")

Class Inheritance

Create new classes based on existing classes:

Example - Vehicle inheritance hierarchy:

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.is_running = False
    
    def start(self):
        self.is_running = True
        return f"{self.make} {self.model} started"
    
    def stop(self):
        self.is_running = False
        return f"{self.make} {self.model} stopped"
    
    def info(self):
        return f"{self.year} {self.make} {self.model}"

class Car(Vehicle):
    def __init__(self, make, model, year, doors):
        super().__init__(make, model, year)
        self.doors = doors
    
    def honk(self):
        return "Beep beep!"
    
    def info(self):
        return f"{super().info()} with {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 wheelie(self):
        return "Doing a wheelie!"
    
    def info(self):
        return f"{super().info()} with {self.engine_size}cc engine"

# Usage
car = Car("Toyota", "Camry", 2022, 4)
bike = Motorcycle("Honda", "CBR", 2021, 600)

print(car.info())       # 2022 Toyota Camry with 4 doors
print(car.start())      # Toyota Camry started
print(car.honk())       # Beep beep!

print(bike.info())      # 2021 Honda CBR with 600cc engine
print(bike.wheelie())   # Doing a wheelie!

Multiple Inheritance

Python supports multiple inheritance where a class can inherit from multiple parent classes:

Example

class Flyable:
    def fly(self):
        return "Flying through the air"

class Swimmable:
    def swim(self):
        return "Swimming in water"

class Duck(Flyable, Swimmable):
    def __init__(self, name):
        self.name = name
    
    def quack(self):
        return f"{self.name} says quack!"

class Penguin(Swimmable):
    def __init__(self, name):
        self.name = name
    
    def slide(self):
        return f"{self.name} is sliding on ice"

# Usage
duck = Duck("Donald")
penguin = Penguin("Pingu")

print(duck.quack())     # Donald says quack!
print(duck.fly())       # Flying through the air
print(duck.swim())      # Swimming in water

print(penguin.swim())   # Swimming in water
print(penguin.slide())  # Pingu is sliding on ice

Abstract Base Classes

Define interfaces that subclasses must implement:

Example

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass
    
    def description(self):
        return f"This is a {self.__class__.__name__} with area {self.area()}"

class Rectangle(Shape):
    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)

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

# Usage
rectangle = Rectangle(5, 3)
circle = Circle(4)

print(rectangle.description())
print(f"Rectangle perimeter: {rectangle.perimeter()}")
print(circle.description())
print(f"Circle perimeter: {circle.perimeter():.2f}")

Composition vs Inheritance

Sometimes composition is better than inheritance:

Example - Composition approach:

class Engine:
    def __init__(self, horsepower, fuel_type):
        self.horsepower = horsepower
        self.fuel_type = fuel_type
        self.is_running = False
    
    def start(self):
        self.is_running = True
        return f"{self.horsepower}HP {self.fuel_type} engine started"
    
    def stop(self):
        self.is_running = False
        return f"Engine stopped"

class GPS:
    def __init__(self):
        self.current_location = "Unknown"
    
    def navigate_to(self, destination):
        return f"Navigating to {destination}"

class Car:
    def __init__(self, make, model, engine, has_gps=False):
        self.make = make
        self.model = model
        self.engine = engine  # Composition
        self.gps = GPS() if has_gps else None  # Composition
    
    def start(self):
        return self.engine.start()
    
    def navigate(self, destination):
        if self.gps:
            return self.gps.navigate_to(destination)
        return "No GPS available"

# Usage
engine = Engine(300, "Gasoline")
car = Car("BMW", "M3", engine, has_gps=True)

print(car.start())                    # 300HP Gasoline engine started
print(car.navigate("New York"))       # Navigating to New York

Practical Example: Bank Account System

Complete banking system example:

from datetime import datetime
from abc import ABC, abstractmethod

class Transaction:
    def __init__(self, transaction_type, amount, description=""):
        self.timestamp = datetime.now()
        self.type = transaction_type
        self.amount = amount
        self.description = description
    
    def __str__(self):
        return f"{self.timestamp.strftime('%Y-%m-%d %H:%M')} - {self.type}: ${self.amount:.2f} - {self.description}"

class Account(ABC):
    def __init__(self, account_number, owner_name, initial_balance=0):
        self.account_number = account_number
        self.owner_name = owner_name
        self._balance = initial_balance
        self.transactions = []
        self._add_transaction("OPENING", initial_balance, "Account opened")
    
    @property
    def balance(self):
        return self._balance
    
    def _add_transaction(self, transaction_type, amount, description=""):
        transaction = Transaction(transaction_type, amount, description)
        self.transactions.append(transaction)
    
    def deposit(self, amount, description="Deposit"):
        if amount > 0:
            self._balance += amount
            self._add_transaction("DEPOSIT", amount, description)
            return True
        return False
    
    @abstractmethod
    def withdraw(self, amount, description="Withdrawal"):
        pass
    
    def get_statement(self):
        statement = f"\n--- Account Statement for {self.owner_name} ---\n"
        statement += f"Account Number: {self.account_number}\n"
        statement += f"Current Balance: ${self.balance:.2f}\n\n"
        statement += "Recent Transactions:\n"
        for transaction in self.transactions[-10:]:  # Last 10 transactions
            statement += str(transaction) + "\n"
        return statement

class SavingsAccount(Account):
    def __init__(self, account_number, owner_name, initial_balance=0, interest_rate=0.02):
        super().__init__(account_number, owner_name, initial_balance)
        self.interest_rate = interest_rate
    
    def withdraw(self, amount, description="Withdrawal"):
        if 0 < amount <= self._balance:
            self._balance -= amount
            self._add_transaction("WITHDRAWAL", amount, description)
            return True
        return False
    
    def add_interest(self):
        interest = self._balance * self.interest_rate / 12  # Monthly interest
        self.deposit(interest, "Monthly interest")

class CheckingAccount(Account):
    def __init__(self, account_number, owner_name, initial_balance=0, overdraft_limit=100):
        super().__init__(account_number, owner_name, initial_balance)
        self.overdraft_limit = overdraft_limit
    
    def withdraw(self, amount, description="Withdrawal"):
        if 0 < amount <= (self._balance + self.overdraft_limit):
            self._balance -= amount
            self._add_transaction("WITHDRAWAL", amount, description)
            if self._balance < 0:
                self._add_transaction("OVERDRAFT FEE", 35, "Overdraft fee")
                self._balance -= 35
            return True
        return False

# Usage
savings = SavingsAccount("SAV001", "Alice Johnson", 1000, 0.03)
checking = CheckingAccount("CHK001", "Bob Smith", 500, 200)

# Perform transactions
savings.deposit(500, "Salary deposit")
savings.withdraw(200, "ATM withdrawal")
savings.add_interest()

checking.deposit(300, "Cash deposit")
checking.withdraw(900, "Rent payment")  # This will trigger overdraft

print(savings.get_statement())
print(checking.get_statement())

Instance vs Class Attributes

Class attributes are shared by all instances; instance attributes belong to one object.

class Circle:
    pi = 3.14159            # class attribute (shared)

    def __init__(self, radius):
        self.radius = radius  # instance attribute (per object)

    def area(self):
        return Circle.pi * self.radius ** 2

a = Circle(2)
b = Circle(3)
print(a.area(), b.area())   # 12.56636 28.27431
print(Circle.pi)            # shared value

Static and Class Methods

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @classmethod
    def from_fahrenheit(cls, f):     # alternative constructor
        return cls((f - 32) * 5 / 9)

    @staticmethod
    def is_freezing(celsius):        # utility, no self/cls
        return celsius <= 0

t = Temperature.from_fahrenheit(212)
print(t.celsius)                     # 100.0
print(Temperature.is_freezing(-5))   # True

Use @classmethod for alternative constructors and @staticmethod for helpers that logically belong to the class but need no instance.

Try It Yourself

Exercise 1: Give Circle a method circumference() returning 2 · pi · radius.

Show solution
class Circle:
    pi = 3.14159
    def __init__(self, r):
        self.r = r
    def circumference(self):
        return 2 * Circle.pi * self.r

print(round(Circle(5).circumference(), 2))   # 31.42

Exercise 2: Add a @classmethod that builds a Circle from a diameter.

Show solution
class Circle:
    def __init__(self, r):
        self.r = r
    @classmethod
    def from_diameter(cls, d):
        return cls(d / 2)

print(Circle.from_diameter(10).r)   # 5.0

Key Takeaways

  • Instance attributes live on self; class attributes are shared.
  • @classmethod receives the class (cls); @staticmethod receives neither.
  • Class methods make great alternative constructors.

📘 Real-World Deep Dive

Classes bundle state and behaviour. Knowing the modern <code>@dataclass(slots=True, frozen=True)</code> form, plus composition over inheritance, lets you write objects that are cheap to reason about and fast at runtime.

Real-Life Scenario

A "Money" value object with currency-safe arithmetic; a "Wallet" class that holds Money and delegates arithmetic.

Real-Life Example

from dataclasses import dataclass, field, replace
from typing import Self

@dataclass(slots=True, frozen=True)
class Money:
    amount_cents: int
    currency: str = "USD"

    def __post_init__(self) -> None:
        if self.currency.upper() not in {"USD", "EUR", "GBP", "JPY"}:
            raise ValueError(f"unsupported currency: {self.currency}")
        object.__setattr__(self, "currency", self.currency.upper())

    def __add__(self, other: Self) -> Self:
        if self.currency != other.currency:
            raise ValueError("cannot add mismatched currencies")
        return replace(self, amount_cents=self.amount_cents + other.amount_cents)

    def __str__(self) -> str:
        return f"{self.amount_cents/100:.2f} {self.currency}"

@dataclass(slots=True)
class Wallet:
    balances: dict[str, Money] = field(default_factory=dict)

    def deposit(self, m: Money) -> None:
        cur = self.balances.get(m.currency, Money(0, m.currency))
        self.balances[m.currency] = cur + m

w = Wallet()
for m in [Money(1999, "usd"), Money(500, "eur"), Money(750, "usd")]:
    w.deposit(m)
for cur, bal in w.balances.items():
    print(cur, bal)

Expected Output

USD 27.49 USD
EUR 5.00 EUR

Common mistakes

  • Mutating a frozen dataclass raises FrozenInstanceError — use dataclasses.replace instead.
  • Custom __eq__ without __hash__ makes the class unhashable; @dataclass(frozen=True) handles both for free.
  • Inheriting from a non-dataclass for "type-is-what-it-is" semantics is fine; inheriting for code reuse usually isn't.

🚀 Performance & Best Practices

  • @dataclass(slots=True) cuts memory and speeds attribute access — almost always worth the flag (3.10+).
  • Prefer composition: a Wallet holds Money; Money doesn't know about wallets.
  • Caching numeric computations on functools.cached_property avoids recomputing on every access.

🧪 Try It Yourself

  1. Add a Wallet.withdraw(amount, currency) that raises if balance is insufficient.
  2. Implement __mul__ so money * 1.08 works (with rounding to 1-cent precision).
  3. Replace the dict-of-balances with a single flat list and benchmark.

FAQ: Python Classes/Objects

Common questions about this page.

What is Python Classes/Objects?

Python Classes/Objects is a Python Tutorial lesson that explains python classes and objects in Python. Deep dive into Python classes and objects with advanced concepts and practical examples. Copy the samples and run them in the Python editor. It is written for beginners who want a clear definition and working examples.

Should I run python classes and objects examples locally for better learning?

Yes. Use the browser editor on StudyGrid for a quick check, then Download the example and run it on your computer. Local runs show real errors and the real toolchain, which is one of the fastest ways to learn python classes and objects in this Python Tutorial Python lesson (Python Classes/Objects).

How do I use python classes and objects in Python?

To use python classes and objects in Python, follow the examples on this StudyGrid page. Copy a snippet, run it in the browser, then Download and run it locally for better learning. Change the values and compare the output.

What is the syntax of python classes and objects?

This Python Classes/Objects tutorial shows python classes and objects syntax with short Python examples. Use the code blocks in this lesson for the exact statements, then try them in your editor.

Python Classes/Objects example for beginners

Yes. This page includes a beginner python classes and objects example you can copy and run. It is designed for searches such as "python classes and objects for beginners", "python classes and objects example", and "how to use python classes and objects".

What are common mistakes with python classes and objects?

Common python classes and objects mistakes include wrong syntax, mixing types, and skipping practice. Work through this Python Tutorial chapter in order, run every example, and check the output before moving on.

Why should I learn python classes and objects?

Python Classes/Objects is used in real Python work. Learning python classes and objects helps you write clearer programs and continue the Python Tutorial tutorial on StudyGrid.

Is Python Classes/Objects free to learn online?

Yes. You can learn python classes and objects free on StudyGrid (studygrid.in). This chapter is part of the Python Tutorial path and includes examples, syntax, and next-step links.