So far we have written functions that operate on data. Object-oriented programming (OOP) flips that: it bundles data and the functions that act on it into a single unit called an object. This chapter introduces the four pillars of OOP through small, runnable examples.
A class is a blueprint; an object is a concrete instance built from that blueprint. The class says what data a robot has and what it can do; each robot object carries its own data.
class Robot:
def __init__(self, name):
self.name = name
def greet(self):
return "Hello, I am " + self.name
r1 = Robot("R2")
r2 = Robot("C3")
print(r1.greet()) # Hello, I am R2
print(r2.greet()) # Hello, I am C3
The __init__ method runs automatically when you create an object. Its first parameter, self, refers to the particular object being built, so each instance keeps its own name.
An instance attribute belongs to one object. A class attribute is shared by every object of the class:
class Dog:
species = "Canis familiaris" # class attribute
def __init__(self, name):
self.name = name # instance attribute
d1 = Dog("Bruno")
d2 = Dog("Rex")
print(d1.species) # Canis familiaris - shared
print(d2.name) # Rex - own value
A method is a function defined inside a class. It always receives the object as its first argument, conventionally named self:
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
c = Counter()
print(c.increment()) # 1
print(c.increment()) # 2
self in the method signature or omitting it when reading attributes. Every attribute access inside a method needs self. in front of it.
BankAccount class with balance, deposit(amount), and withdraw(amount) methods that refuse to go below zero.Student class storing name and a list of grades, with a method returning the average grade.Book class with title, author, and a flag available, plus methods to borrow and return it.class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
else:
print("Insufficient funds")
acc = BankAccount(100)
acc.deposit(50)
acc.withdraw(200) # Insufficient funds
print(acc.balance) # 150
Inheritance lets a new class reuse the code of an existing one. The child class gets every attribute and method of the parent, and can add or replace its own.
class Animal:
def speak(self):
return "Some sound"
class Cat(Animal):
def speak(self):
return "Meow"
class Dog(Animal):
def speak(self):
return "Woof"
animals = [Cat(), Dog(), Animal()]
for a in animals:
print(a.speak()) # Meow / Woof / Some sound
Here Cat and Dog inherit from Animal but override speak. Shared logic stays in the parent; differences live in the children.
Polymorphism means "many forms": the same method name behaves differently depending on the object type. The loop above is a working example — speak() on each object produces a different result even though the call looks identical.
def announce(creature):
print(creature.speak())
announce(Cat()) # Meow
announce(Dog()) # Woof
Because every animal promises a speak() method, one function works for all of them.
Encapsulation hides internal details behind a clean interface, so callers use methods instead of touching raw data. Python signals a private member with a leading underscore — it is a convention, not a hard wall:
class Thermostat:
def __init__(self):
self._temp_c = 22
def raise_temp(self, delta):
self._temp_c += delta
def current(self):
return self._temp_c
t = Thermostat()
t.raise_temp(3)
print(t.current()) # 25
Abstraction shows only what matters and hides how things are done. The user of a Thermostat calls raise_temp without knowing whether the wiring is electric or gas. In code, the abc module enforces this with abstract base classes that cannot be instantiated and must be implemented by subclasses:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
s = Square(4)
print(s.area()) # 16
Any class that forgets to implement area() fails immediately, which keeps the contract honest.
Vehicle parent class with fuel and move(), then Car and Bicycle children that override move() differently.vehicle_count to Vehicle that increments every time a new object is created, proving the attribute is shared.BankAccount hierarchy: a parent Account with balance and interest(), and a SavingsAccount child that overrides the interest calculation.Item base class (title, availability), then Book and DVD subclasses with different loan_period values. Add encapsulation by keeping availability private with public borrow()/return_item() methods, and verify both subclasses work through one shared function that only calls the public interface.