Real Programs
Object-Oriented Python
Classes in Python: init and self, instance vs class attributes, methods, properties, inheritance with super(), polymorphism, and dunder methods like str and eq.
Everything in Python is an object — numbers, strings, functions, even classes. You define your own types with class.
class Student:
school = "Kalvium" # class attribute: shared by all students
def __init__(self, name, roll): # the constructor
self.name = name # instance attributes: one per object
self.roll = roll
self.marks = []
def add_mark(self, m): # a method: first parameter is always self
self.marks.append(m)
def average(self):
return sum(self.marks) / len(self.marks) if self.marks else 0
s1 = Student("Asha", 101)
s2 = Student("Ravi", 102)
s1.add_mark(88); s1.add_mark(94)
s2.add_mark(76)
print(s1.name, s1.average(), s1.school)
print(s2.name, s2.average(), Student.school)Asha 91.0 Kalvium Ravi 76.0 Kalvium
self is the object the method was called on — the same idea as this in C++, but written explicitly.
Encapsulation by convention
Python has no private keyword. By convention:
_name— "internal, please don't touch".__name— name-mangled to_ClassName__name, which makes accidental access harder.
Properties let you add validation while keeping attribute syntax:
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
@property
def balance(self): # read with acc.balance
return self._balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self._balance += amount
acc = Account("Priya", 1000)
acc.deposit(500)
print(acc.balance)
try:
acc.balance = 10**6 # no setter defined: read-only
except AttributeError as e:
print("blocked:", e)1500 blocked: property 'balance' of 'Account' object has no setter
Inheritance and super()
class Person:
def __init__(self, name, age):
self.name, self.age = name, age
def introduce(self):
return f"I am {self.name}, {self.age}."
class Student(Person):
def __init__(self, name, age, course):
super().__init__(name, age) # run the parent's constructor
self.course = course
def introduce(self): # override
return super().introduce() + f" I study {self.course}."
class Teacher(Person):
def introduce(self):
return f"Good morning, I am Prof. {self.name}."
people = [Student("Meera", 19, "DSA"), Teacher("Rao", 45), Person("Kiran", 30)]
for p in people:
print(p.introduce()) # polymorphism: each object's own version
print(isinstance(people[0], Person), issubclass(Teacher, Person))I am Meera, 19. I study DSA. Good morning, I am Prof. Rao. I am Kiran, 30. True True
Every method in Python behaves like a C++ virtual function: the call is resolved on the actual object at run time. Python also supports multiple inheritance (class C(A, B)), resolving names with the method resolution order (C.__mro__).
Dunder (magic) methods
Methods with double underscores let your objects work with Python's built-in syntax — this is Python's operator overloading:
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self): # print(v) and str(v)
return f"({self.x}, {self.y})"
def __repr__(self): # developer view, used in lists
return f"Vector({self.x}, {self.y})"
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, k): # v * 3
return Vector(self.x * k, self.y * k)
def __eq__(self, other): # v1 == v2
return (self.x, self.y) == (other.x, other.y)
def __len__(self): # len(v)
return 2
def __abs__(self): # abs(v): the length
return (self.x ** 2 + self.y ** 2) ** 0.5
a, b = Vector(3, 4), Vector(1, 2)
print(a + b, a * 3, abs(a), a == Vector(3, 4), len(a))
print([a, b])(4, 6) (9, 12) 5.0 True 2 [Vector(3, 4), Vector(1, 2)]
| Method | Enables |
|---|---|
__init__ |
construction: Vector(3, 4) |
__str__, __repr__ |
print(obj), repr(obj) |
__add__, __sub__, __mul__ |
+, -, * |
__eq__, __lt__ |
==, < (and sorting) |
__len__, __getitem__ |
len(obj), obj[i] |
__iter__, __next__ |
for x in obj |
__call__ |
obj() |
Class methods, static methods and dataclasses
from dataclasses import dataclass
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(c): # utility: needs neither self nor cls
return c <= 0
t = Temperature.from_fahrenheit(98.6)
print(round(t.celsius, 1), Temperature.is_freezing(-3))
@dataclass # writes __init__, __repr__ and __eq__ for you
class Book:
title: str
author: str
pages: int = 0
print(Book("Clean Code", "Robert Martin", 464))37.0 True Book(title='Clean Code', author='Robert Martin', pages=464)
Test yourself
4 questions · pick an answer to check it instantly
- 1
What is
selfin a method?selfrefers to the instance; the name is a strong convention, not a keyword. - 2
Where should data shared by all objects of a class be stored?
Class attributes are shared; instance attributes belong to each object. - 3
What does
super().__init__(name)do in a child class?It lets the parent initialise its part of the object. - 4
Which dunder method makes
print(obj)show a friendly string?printcallsstr(obj), which calls__str__.