In the previous chapter, we looked at Functions and how they let a program organise code into reusable, named blocks. Object-Oriented Programming, commonly abbreviated as OOP, takes this idea of organisation a step further by grouping related data and the functions that operate on that data together into a single unit, instead of keeping variables and functions scattered separately throughout a program. This chapter introduces the core ideas behind Object-Oriented Programming in Python: classes, objects, and the four principles that define how object-oriented code is typically structured.
As programs grow larger and start modelling real-world things, such as a user, a bank account, or in keeping with the examples used throughout this series, a YouTube channel, it becomes increasingly useful to bundle a thing's properties, like its name or subscriber count, together with the actions it can perform, like uploading a video or gaining subscribers. Object-Oriented Programming provides exactly this kind of structure, and Python fully supports it as one of its core programming styles.
Object-Oriented Programming is a style of programming built around the idea of organising code into classes and objects. A class acts as a blueprint describing what properties and behaviours something should have, while an object is an actual, individual instance created from that blueprint. This approach makes it easier to model real-world entities in code, keeps related data and functionality bundled together in one place, and makes larger programs considerably easier to organise, extend, and maintain over time.
A class is defined using the class keyword, and it describes the properties, known as attributes, and the functions, known as methods, that any object created from that class will have. An object is a specific instance of a class, created by calling the class name as though it were a function. A single class can be used to create as many separate objects as needed, each with its own independent set of attribute values.
class YouTubeChannel:
def show_intro(self):
print("This is a YouTube channel object.")
channel1 = YouTubeChannel()
channel1.show_intro()
This is a YouTube channel object.
Here, YouTubeChannel is a class acting as a blueprint, and channel1 is an object created from that class, capable of calling the show_intro method defined inside it.
When an object is created from a class, Python automatically calls a special method named __init__, if one has been defined, allowing the object's initial attribute values to be set up right at the moment it's created. Every method defined inside a class, including __init__, includes self as its first parameter, which refers to the specific object the method is currently being called on, allowing that method to access and modify that particular object's own attributes.
class YouTubeChannel:
def __init__(self, name, subscribers):
self.name = name
self.subscribers = subscribers
def show_details(self):
print(self.name, "has", self.subscribers, "subscribers.")
channel1 = YouTubeChannel("CS Engineering Gyan", 45000)
channel1.show_details()
CS Engineering Gyan has 45000 subscribers.
Here, the __init__ method runs automatically as soon as channel1 is created, storing the values "CS Engineering Gyan" and 45000 as attributes on that specific object using self.
Object-Oriented Programming is generally described in terms of four core principles: Encapsulation, Inheritance, Polymorphism, and Abstraction. Each of these principles addresses a different aspect of how object-oriented code is structured and behaves, and together they form the foundation of writing clean, well-organised, object-oriented Python programs.
Encapsulation refers to bundling an object's data, its attributes, together with the methods that operate on that data, inside a single class, while also controlling how that data can actually be accessed or modified from outside the class. In Python, a naming convention using a leading underscore is commonly used to indicate that an attribute is intended to be treated as internal to the class, discouraging direct access from outside code in favour of using the class's own methods instead.
class YouTubeChannel:
def __init__(self, name, subscribers):
self.name = name
self._subscribers = subscribers
def gain_subscribers(self, count):
self._subscribers += count
def show_subscribers(self):
print(self.name, "subscriber count:", self._subscribers)
channel1 = YouTubeChannel("CS Engineering Gyan", 45000)
channel1.gain_subscribers(500)
channel1.show_subscribers()
CS Engineering Gyan subscriber count: 45500
Here, the subscriber count is only ever updated through the gain_subscribers method, rather than being changed directly from outside the class, keeping that logic bundled together and controlled in one place.
Inheritance allows a new class, known as a child or derived class, to reuse the attributes and methods already defined in an existing class, known as a parent or base class, instead of rewriting that same logic again from scratch. The child class can also add its own additional attributes and methods, or override existing ones, on top of everything it already inherits from the parent class.
class YouTubeChannel:
def __init__(self, name, subscribers):
self.name = name
self.subscribers = subscribers
def show_details(self):
print(self.name, "has", self.subscribers, "subscribers.")
class EducationChannel(YouTubeChannel):
def __init__(self, name, subscribers, subject):
super().__init__(name, subscribers)
self.subject = subject
def show_subject(self):
print(self.name, "primarily teaches", self.subject)
channel1 = EducationChannel("CS Engineering Gyan", 45000, "Computer Science")
channel1.show_details()
channel1.show_subject()
CS Engineering Gyan has 45000 subscribers. CS Engineering Gyan primarily teaches Computer Science
Here, EducationChannel inherits from YouTubeChannel, automatically gaining access to the show_details method, while also adding its own new attribute, subject, and its own new method, show_subject.
Polymorphism allows different classes to define a method with the exact same name, while each class provides its own specific implementation of what that method actually does. This means the same method call can behave differently depending on which particular object it's actually being called on, without the calling code needing to know or care exactly which class that object belongs to.
class EducationChannel:
def describe(self):
print("This channel focuses on educational content.")
class EntertainmentChannel:
def describe(self):
print("This channel focuses on entertainment content.")
channels = [EducationChannel(), EntertainmentChannel()]
for channel in channels:
channel.describe()
This channel focuses on educational content. This channel focuses on entertainment content.
Here, both classes define a method called describe, but each one produces a different result, and the same loop is able to call describe() on every object in the list without needing to know in advance which specific class each object belongs to.
Abstraction involves exposing only the essential details a user of a class actually needs to interact with it, while hiding the more complex internal implementation details behind that simpler interface. A method can be called without its user needing to understand exactly how it works internally, only what it does and what result it produces.
class YouTubeChannel:
def __init__(self, name, subscribers):
self.name = name
self.subscribers = subscribers
def _calculate_growth_bonus(self):
return self.subscribers * 0.02
def show_growth_bonus(self):
bonus = self._calculate_growth_bonus()
print(self.name, "estimated monthly growth:", bonus)
channel1 = YouTubeChannel("CS Engineering Gyan", 45000)
channel1.show_growth_bonus()
CS Engineering Gyan estimated monthly growth: 900.0
Here, calling show_growth_bonus() is all a user of this class actually needs to do; the underlying calculation performed inside _calculate_growth_bonus remains hidden away as an internal implementation detail they don't need to worry about.
| Principle | What It Achieves |
|---|---|
| Encapsulation | Bundles data and methods together, controlling how data is accessed or modified |
| Inheritance | Allows a class to reuse and extend the attributes and methods of another class |
| Polymorphism | Lets the same method name behave differently depending on the object it's called on |
| Abstraction | Hides complex implementation details behind a simpler, easier-to-use interface |
| Mistake | Correct Practice |
|---|---|
| Forgetting to include self as a method's first parameter. | Every method inside a class needs self as its first parameter to access that object's own attributes. |
| Confusing a class with an object. | A class is the blueprint; an object is an actual instance created from that blueprint using the class name. |
| Assuming inheritance means copying and pasting code between classes. | Inheritance lets a child class automatically reuse a parent class's attributes and methods without duplicating any code. |
| Mixing up polymorphism with simply having multiple classes. | Polymorphism specifically refers to different classes implementing a method with the same name differently. |
Object-Oriented Programming lets a Python program organise data and behaviour together into classes and objects, rather than keeping them scattered separately throughout the code. We looked at how to define a class and create objects from it, how the __init__ constructor and the self keyword work together to set up an object's attributes, and the four pillars of OOP — Encapsulation, Inheritance, Polymorphism, and Abstraction — each contributing a different way of keeping object-oriented code organised, reusable, and easy to work with.
With a solid understanding of how classes and objects work in Python, you are now ready to move on to file handling, which explains how a Python program can read from and write to files stored outside the program itself.