Online admissions open for the SEO, PHP web development and networking & cybersecurity courses. Enquire for the next batch.

Object-Oriented Programming in Python: Classes, Objects and Inheritance

6 min read Updated By the SkillNewton team

What is object-oriented programming in Python?

Object-oriented programming (OOP) in Python is a way of organising code around objects that bundle data (attributes) with the functions that act on it (methods). You define a blueprint with the class keyword and create as many independent instances from it as you need. Inheritance, properties and special dunder methods let your objects share behaviour and work naturally with built-in Python features.

Classes, instances, attributes and methods

  • Class: a blueprint that defines what data an object holds and what it can do.
  • Instance (object): one concrete thing built from a class, with its own data.
  • Attribute: a value stored on an object or class, read with dot notation such as book.title.
  • Method: a function defined inside a class. Its first parameter, self, is the instance it was called on.

OOP builds on the variables and functions covered in Python basics for beginners. If you have used classes in PHP, the ideas are the same, though Python passes self explicitly instead of using $this.

How __init__, self and class attributes work

When you call a class like a function, Python creates a new object and runs __init__ on it. Attributes assigned to self belong to that one instance. Attributes assigned in the class body are class attributes, shared by every instance.

class Book:
    loan_days = 14                     # class attribute, shared

    def __init__(self, title, author):
        self.title = title             # instance attributes
        self.author = author
        self.on_loan = False

    def lend(self):
        if self.on_loan:
            raise ValueError(f"{self.title} is already on loan")
        self.on_loan = True


b1 = Book("Godan", "Premchand")
b2 = Book("Malgudi Days", "R. K. Narayan")
b1.lend()
print(b1.on_loan, b2.on_loan)          # True False
print(Book.loan_days, b2.loan_days)    # 14 14

b1.lend() is shorthand for Book.lend(b1), which is why every method needs self. Keep mutable values such as lists out of class attributes unless you really want every instance to share them.

Properties, class methods and static methods

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius         # goes through the setter below

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Below absolute zero")
        self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32

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

    @staticmethod
    def is_fever(celsius):
        return celsius >= 38.0


t = Temperature.from_fahrenheit(98.6)
print(round(t.celsius, 1))             # 37.0
print(Temperature.is_fever(38.5))      # True
  • @property makes a method readable like an attribute and lets a setter validate new values, without changing how callers use the object.
  • @classmethod receives the class as cls. It is the usual way to write alternative constructors such as from_fahrenheit.
  • @staticmethod receives neither the instance nor the class. Use it for a helper that belongs with the class logically but needs no object state.

Inheritance, super(), overriding and dunder methods

A subclass inherits everything from its parent and can override methods with its own version. super() calls the parent's implementation, most often inside __init__. Dunder (double underscore) methods hook your objects into built-in behaviour.

import math


class Shape:
    def __init__(self, name):
        self.name = name

    def area(self):
        raise NotImplementedError

    def __str__(self):
        return f"{self.name} with area {self.area():.2f}"


class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__("Rectangle")
        self.width = width
        self.height = height

    def area(self):                    # overrides Shape.area
        return self.width * self.height

    def __repr__(self):
        return f"Rectangle(width={self.width}, height={self.height})"

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


class Circle(Shape):
    def __init__(self, radius):
        super().__init__("Circle")
        self.radius = radius

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


for shape in [Rectangle(3, 4), Circle(1)]:
    print(shape)                       # uses __str__
print(repr(Rectangle(3, 4)))           # Rectangle(width=3, height=4)
print(Rectangle(3, 4) == Rectangle(3, 4))   # True

__str__ gives a friendly description for print(); __repr__ gives an unambiguous one for developers, ideally looking like code that recreates the object. Defining __eq__ makes instances unhashable unless you also define __hash__, so they cannot be used as dict keys or set members by default.

Dataclasses, and composition versus inheritance

For classes that mainly hold data, the dataclasses module writes __init__, __repr__ and __eq__ for you from type-annotated fields.

from dataclasses import dataclass, field


@dataclass
class Member:
    name: str
    member_id: int
    borrowed: list[str] = field(default_factory=list)


@dataclass
class Library:
    members: dict[int, Member] = field(default_factory=dict)

    def join(self, member):
        self.members[member.member_id] = member


lib = Library()
lib.join(Member("Asha", 101))
print(lib.members[101])     # Member(name='Asha', member_id=101, borrowed=[])

Library has members rather than being a kind of member. That is composition: one object holds others and delegates work to them. Use inheritance only for a genuine "is a" relationship, such as a circle being a shape; prefer composition when you just want to reuse behaviour, because deep inheritance chains are hard to change.

Designing a class step by step

  1. Write down the nouns in your problem; each important one is a candidate class, such as Account or Customer.
  2. List the data each one must hold, and decide which values belong to each instance and which are shared.
  3. List the actions as verbs; these become methods, such as deposit and withdraw.
  4. Protect rules inside the class: validate in __init__, setters and methods so an object can never reach an invalid state.
  5. Add __repr__ early so debugging output is readable.
  6. Only then look for shared behaviour that justifies a parent class, and test each class on its own before combining them.

This planning step mirrors the design phase of the software development life cycle, just on a smaller scale.

Practical exercise: model a bank account

class InsufficientFunds(Exception):
    pass


class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal must be positive")
        if amount > self._balance:
            raise InsufficientFunds(f"Balance is only {self._balance}")
        self._balance -= amount

    def __repr__(self):
        return f"BankAccount(owner={self.owner!r}, balance={self._balance})"


acct = BankAccount("Kiran", 5000)
acct.deposit(1500)
acct.withdraw(2000)
print(acct.balance)          # 4500
print(acct)                  # BankAccount(owner='Kiran', balance=4500)
  • Add a SavingsAccount subclass that takes an interest rate, calls super().__init__(), and has an add_interest() method.
  • Add a Bank class that holds accounts in a dict by account number and offers transfer(from_no, to_no, amount).
  • Done looks like this: a transfer larger than the balance raises InsufficientFunds and leaves both balances unchanged, and acct.balance = 0 raises AttributeError because the property has no setter.

For real money, store amounts as whole paise in an int or use decimal.Decimal, because floats cannot represent most decimal fractions exactly. If you would like to practise these ideas in another language, the PHP Foundation course teaches the same fundamentals in PHP.

Common mistakes

  • Forgetting self as the first method parameter, which causes a TypeError about the number of arguments.
  • Storing a list or dict as a class attribute and then being surprised when every instance changes together.
  • Overriding __init__ in a subclass without calling super().__init__(), so the parent's attributes are never set.
  • Building deep inheritance hierarchies to share a little code when composition would be simpler.
  • Writing getter and setter methods for every attribute instead of using plain attributes and adding @property only when validation is needed.

Frequently asked questions

Does Python have private attributes?

Not enforced ones. A single leading underscore, as in _balance, is a convention meaning internal use. A double underscore triggers name mangling, which makes accidental access harder but does not prevent it.

What is the difference between __str__ and __repr__?

__str__ is for end users and is used by print() and str(). __repr__ is for developers and is used in the REPL and inside containers. If only __repr__ exists, Python uses it for both.

Can a Python class inherit from more than one class?

Yes, Python supports multiple inheritance and resolves methods using the method resolution order, which you can inspect with ClassName.__mro__. Use it sparingly, typically for small mixin classes.

Want to Learn This Properly?

The PHP Web Development: Foundation & Practical Implementation (Online) turns guides like this into practised skills, with projects on real websites.

WhatsApp Call Us Now

Free counselling

Send an Enquiry

Tell us which course you are interested in. We reply by email or phone with the next batch and how to enrol.

We reply by email or phone within working hours. No spam.