Programming with Python | Chapter 12: Object-Oriented Programming (OOP) – Classes and Objects
Chapter Objectives
- Understand the basic principles of Object-Oriented Programming (OOP).
- Differentiate between procedural programming and OOP.
- Define classes as blueprints for creating objects.
- Understand objects (instances) as concrete realizations of classes.
- Learn how to define a class using the
classkeyword. - Define attributes (data associated with an object or class).
- Understand instance attributes (unique to each object).
- Understand class attributes (shared by all instances of a class).
- Define methods (functions associated with a class) to represent object behavior.
- Understand the special
__init__method (constructor) for initializing objects. - Understand the role of the
selfparameter in instance methods. - Learn how to create (instantiate) objects from a class.
Introduction
So far, we’ve primarily used procedural programming: organizing code into functions that operate on data. Object-Oriented Programming (OOP) offers a different way to structure programs by bundling data and the functions that operate on that data together into units called objects. This chapter introduces the core concepts of OOP in Python: classes and objects. A class acts as a blueprint or template, defining the properties (attributes) and behaviors (methods) that all objects of a certain type will share. An object is a specific instance created from that class blueprint. OOP helps create modular, reusable, and often more intuitive code, especially for complex systems that model real-world entities.
Theory & Explanation
Procedural vs. Object-Oriented Programming
- Procedural: Focuses on procedures (functions) that perform operations on data. Data and functions are typically separate. Think of writing a series of instructions or recipes. (e.g., C, early Python scripts).
- Object-Oriented: Focuses on “objects” which combine data (attributes) and behavior (methods) into a single unit. Programs are designed as interactions between these objects. Think of modeling real-world things and their interactions. (e.g., Java, C++, Python).
OOP aims to manage complexity by modeling concepts as objects, leading to benefits like:
- Encapsulation: Bundling data and methods within an object, potentially hiding internal details.
- Inheritance: Creating new classes that reuse and extend properties from existing classes (covered later).
- Polymorphism: Allowing objects of different classes to respond to the same method call in different ways (covered later).
graph LR
subgraph proc["Procedural Programming"]
direction LR
P_Data["Data Structures"] --> P_Func["Functions Operate on Data"]
end
subgraph oop["Object-Oriented Programming (OOP)"]
direction LR
O_Data["Attributes / Data"] --> O_Obj["Objects<br/>(Data + Behavior)"]
O_Behav["Methods / Behavior"] --> O_Obj
O_Obj --> O_Int["Objects Interact"]
end
P_Func --- P_Data
style proc fill:#E0F2FE,stroke:#0284C7,stroke-width:1px
style oop fill:#EDE9FE,stroke:#5B21B6,stroke-width:1px
Classes and Objects: The Blueprint and the Instance
- Class: A blueprint, template, or definition for creating objects. It defines a set of attributes (variables) that objects of the class will have and methods (functions) that objects of the class can perform. The class itself doesn’t do anything; it just defines the structure and behavior.
- Analogy: The architectural blueprint for a house.
- Object (Instance): A specific realization created from a class. Each object has its own set of attribute values (its state) but shares the methods defined by the class. Multiple objects can be created from the same class.
- Analogy: An actual house built according to the blueprint. You can build many houses (objects) from the same blueprint (class).
Defining a Class (class)
You define a class using the class keyword, followed by the class name (conventionally in CamelCase or PascalCase), and a colon :. The indented block contains the class definition, including attributes and methods.
Syntax:
class ClassName:
"""Optional docstring describing the class."""
# Class attribute (shared by all instances)
class_attribute = value
# Initializer method (constructor)
def __init__(self, parameter1, parameter2, ...):
"""Initializes a new instance of the class."""
# Instance attributes (unique to each instance)
self.instance_attribute1 = parameter1
self.instance_attribute2 = value_or_calculation
# ...
# Instance method
def method_name(self, parameter1, ...):
"""Performs some action related to the object."""
# Code block using self.instance_attribute, parameters, etc.
# ...
return result # Optional
graph LR
Class["Dog Class"]
subgraph ClassDef ["Blueprint: class Dog"]
direction LR
A["Attributes:<br/>- species (Class)<br/>- name (Instance)<br/>- breed (Instance)"]
M["Methods:<br/>- __init__(self, name, breed)<br/>- describe(self)<br/>- add_trick(self, trick)"]
end
subgraph Instances ["Objects"]
direction TB
O1["Object: dog1<br/>name: 'Buddy'<br/>breed: 'GoldenDoodle'"]
O2["Object: dog2<br/>name: 'Lucy'<br/>breed: 'Labrador'"]
O3["Object: dog3<br/>name: 'Max'<br/>breed: 'Poodle'"]
end
Class --> O1
Class --> O2
Class --> O3
ClassDef --> O1
ClassDef --> O2
ClassDef --> O3
style ClassDef fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px
style Instances fill:#F3F4F6,stroke:#9CA3AF,stroke-width:1px
Attributes: Storing Data
Attributes are variables associated with a class or an object.
Class Attributes: Defined directly inside the class block but outside any method. They are shared by all instances (objects) of the class. If you change a class attribute, the change reflects in all instances (unless an instance has overridden it with its own attribute of the same name).
class Dog:
species = "Canis familiaris" # Class attribute
def __init__(self, name):
self.name = name # Instance attribute
dog1 = Dog("Buddy")
dog2 = Dog("Lucy")
print(dog1.species) # Output: Canis familiaris
print(dog2.species) # Output: Canis familiaris
print(Dog.species) # Can also access via the class nameInstance Attributes: Defined inside methods, typically within the __init__ method, using self.attribute_name = value. Each object created from the class gets its own copy of instance attributes. Changes to an instance attribute only affect that specific object.
# Continuing the Dog example above:
print(dog1.name) # Output: Buddy
print(dog2.name) # Output: Lucy
dog1.name = "Buddy Boy" # Change instance attribute for dog1 only
print(dog1.name) # Output: Buddy Boy
print(dog2.name) # Output: Lucy (dog2 is unaffected)Methods: Defining Behavior
Methods are functions defined inside a class. They operate on the data (attributes) associated with an object or the class itself.
Instance Methods:
The most common type. They operate on a specific instance (object) of the class. Their first parameter is always self (by convention, though technically any name works), which refers to the instance calling the method. Python passes the instance automatically when you call the method on an object (my_object.method()).
class Dog:
species = "Canis familiaris"
def __init__(self, name, breed):
self.name = name
self.breed = breed
self.tricks = [] # Each dog gets its own list of tricks
# Instance method (takes 'self' as first parameter)
def describe(self):
"""Returns a description of the dog."""
return f"{self.name} is a {self.breed} ({self.species})."
# Another instance method
def add_trick(self, trick_name):
"""Adds a trick to the dog's list."""
self.tricks.append(trick_name)
print(f"{self.name} learned {trick_name}!")
# Create an instance
my_dog = Dog("Fido", "Golden Retriever")
# Call instance methods on the object
description = my_dog.describe()
print(description) # Output: Fido is a Golden Retriever (Canis familiaris).
my_dog.add_trick("sit") # Output: Fido learned sit!
my_dog.add_trick("fetch") # Output: Fido learned fetch!
print(f"{my_dog.name}'s tricks: {my_dog.tricks}") # Output: Fido's tricks: ['sit', 'fetch']Notice how describe and add_trick use self to access the specific object’s name, breed, and tricks.
The __init__ Method (Constructor/Initializer):
This is a special dunder (double underscore) method. It’s automatically called when you create a new object (instance) from the class (ClassName(...)). Its primary purpose is to initialize the instance attributes of the object. The self parameter refers to the newly created object. Any arguments passed during object creation (after the class name) are passed to __init__ (after self).
# In the Dog class definition above:
# def __init__(self, name, breed):
# self.name = name # Initializes the 'name' attribute for the specific dog object
# self.breed = breed # Initializes the 'breed' attribute
# When we call:
# my_dog = Dog("Fido", "Golden Retriever")
# Python internally does something like:
# 1. Creates an empty Dog object.
# 2. Calls Dog.__init__(<the new object>, "Fido", "Golden Retriever")
# 3. The __init__ method assigns "Fido" to the object's 'name' and "Golden Retriever" to its 'breed'.
# 4. The newly initialized object is assigned to my_dog.
The self Parameter:
self represents the instance of the class on which a method is being called. It allows methods to access and modify the object’s attributes (self.attribute) and call other methods of the object (self.other_method()). It’s always the first parameter in the definition of an instance method, but you don’t provide it explicitly when calling the method on an object (my_object.method(arg1) – Python passes the object my_object as self automatically).
Creating Objects (Instantiation)
Creating an object from a class is called instantiation. You do this by calling the class name as if it were a function, passing any arguments required by the __init__ method.
# Syntax: variable_name = ClassName(arguments_for_init)
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_description(self):
return f"{self.year} {self.make} {self.model}"
def read_odometer(self):
print(f"This car has {self.odometer_reading} miles on it.")
# Instantiate (create) two Car objects
my_car = Car("Toyota", "Camry", 2022)
your_car = Car("Ford", "Mustang", 2023)
# Access attributes and call methods on the specific objects
print(my_car.get_description()) # Output: 2022 Toyota Camry
my_car.read_odometer() # Output: This car has 0 miles on it.
print(your_car.get_description()) # Output: 2023 Ford Mustang
your_car.odometer_reading = 50 # Can modify instance attributes directly (if needed)
your_car.read_odometer() # Output: This car has 50 miles on it.
Code Examples
Example 1: Simple BankAccount Class
# bank_account.py
class BankAccount:
"""Represents a simple bank account."""
# Class attribute (e.g., interest rate - shared, though maybe unrealistic)
interest_rate = 0.01
def __init__(self, account_holder, initial_balance=0.0):
"""Initializes a new bank account."""
# Instance attributes
self.account_holder = account_holder
# Use a leading underscore for 'internal' attributes (convention)
self._balance = float(initial_balance)
def deposit(self, amount):
"""Deposits funds into the account."""
if amount > 0:
self._balance += amount
print(f"Deposited ${amount:.2f}. New balance: ${self._balance:.2f}")
else:
print("Deposit amount must be positive.")
def withdraw(self, amount):
"""Withdraws funds from the account if sufficient balance exists."""
if amount <= 0:
print("Withdrawal amount must be positive.")
elif amount > self._balance:
print(f"Insufficient funds. Cannot withdraw ${amount:.2f}.")
else:
self._balance -= amount
print(f"Withdrew ${amount:.2f}. New balance: ${self._balance:.2f}")
def get_balance(self):
"""Returns the current account balance."""
return self._balance
def display_details(self):
"""Prints the account details."""
print(f"\n--- Account Details ---")
print(f"Holder: {self.account_holder}")
print(f"Balance: ${self.get_balance():.2f}") # Call another method using self
print(f"Interest Rate: {BankAccount.interest_rate:.2%}") # Access class attribute
print(f"-----------------------")
# --- Using the BankAccount class ---
account1 = BankAccount("Alice Smith", 1000.0)
account2 = BankAccount("Bob Johnson") # Uses default initial_balance=0.0
account1.display_details()
account2.display_details()
account1.deposit(500.50)
account1.withdraw(200)
account1.withdraw(2000) # Insufficient funds
account2.deposit(150)
account2.withdraw(20)
account1.display_details()
account2.display_details()
# Accessing balance directly (possible, but using get_balance is preferred)
# print(f"Alice's balance directly: {account1._balance}")
Explanation:
- The
BankAccountclass defines attributes (interest_rate– class,account_holder,_balance– instance) and methods (__init__,deposit,withdraw,get_balance,display_details). __init__sets up the initial state of each account object. Note the use of a leading underscore_balance– this is a convention indicating the attribute is intended for internal use (though Python doesn’t strictly enforce privacy).- Methods like
depositandwithdrawmodify the object’s state (self._balance). display_detailsaccesses both instance attributes (self.account_holder) and class attributes (BankAccount.interest_rate) and calls another instance method (self.get_balance()).- Objects
account1andaccount2are created (instantiated) and methods are called on them. Each object maintains its own balance.
Common Mistakes or Pitfalls
Forgetting self:Forgetting to includeselfas the first parameter in instance method definitions (def my_method():instead ofdef my_method(self):). This leads toTypeError.Forgetting to use self:Inside methods, forgetting to useself.to access instance attributes or other instance methods (attribute_nameinstead ofself.attribute_name). This leads toNameError.- Confusing Class and Instance Attributes: Incorrectly accessing or modifying class attributes when instance attributes were intended, or vice-versa. Remember
ClassName.attributefor class attributes andself.attributefor instance attributes within methods. Incorrect __init__:Misspelling__init__(e.g.,__int__) means the initializer won’t be called automatically during object creation.Calling __init__ Directly:You generally don’t call__init__directly; it’s called automatically during instantiation (ClassName(...)).- Modifying Class Attributes Incorrectly: Assigning to
self.class_attribute = valuecreates an instance attribute that shadows the class attribute for that specific instance, rather than changing the shared class attribute. To change the class attribute for all instances, useClassName.class_attribute = value.
Chapter Summary
| Concept | Syntax / Example | Description |
|---|---|---|
| Class | class MyClass: ... |
A blueprint or template for creating objects. Defines attributes and methods. Uses CamelCase naming convention. |
| Object (Instance) | my_obj = MyClass() |
A specific realization created from a class. Holds its own state (attribute values) but shares class methods. |
| Attribute | self.attr / ClassName.attr |
Variables associated with a class or object, storing data. |
| Instance Attribute | self.name = "value" (inside __init__ or methods) |
Data unique to each specific object (instance). Defined using self. |
| Class Attribute | species = "..." (inside class, outside methods) |
Data shared by all instances of the class. Accessed via ClassName.attribute or self.attribute (if not shadowed by an instance attribute). |
| Method | def my_method(self, ...): ... |
Functions defined inside a class that define the behavior of objects. |
| Instance Method | def action(self, param): ... |
Methods that operate on a specific instance. The first parameter is always self, referring to the instance. |
__init__ Method |
def __init__(self, p1, p2): ... |
Special “initializer” or “constructor” method. Automatically called when an object is created (instantiated) to set up its initial state (instance attributes). |
self Parameter |
self (as first parameter of instance methods) |
A reference to the specific instance on which a method is being called. Used to access instance attributes (self.attr) and other instance methods (self.method()). Passed automatically by Python. |
| Instantiation | my_car = Car("Toyota", "Camry") |
The process of creating an object (instance) from a class by calling the class name like a function. Arguments are passed to __init__. |
- Object-Oriented Programming (OOP) models problems using objects that bundle data (attributes) and behavior (methods).
- A class is a blueprint defined using the
classkeyword. - An object (or instance) is created from a class (
my_obj = ClassName()). - Attributes store data:
- Class attributes are shared by all instances.
- Instance attributes are unique to each object, typically initialized in
__init__.
- Methods are functions defined within a class:
- Instance methods operate on objects; their first parameter is
self. __init__(self, ...)is the special initializer method called automatically when an object is created.selfrefers to the specific instance the method is called on.
- Instance methods operate on objects; their first parameter is
Exercises & Mini Projects
Exercises
Define Person Class:Define a class namedPersonwith an__init__method that takesnameandageas arguments and assigns them to instance attributesself.nameandself.age.Instantiate Person:Create twoPersonobjects with different names and ages. Print thenameandageattributes of each object.Add introduce Method:Add an instance method calledintroduce(self)to thePersonclass. This method should print a string like “Hi, my name is [Name] and I am [Age] years old.” Call this method on bothPersonobjects you created.- Class Attribute: Add a class attribute
species = "Homo sapiens"to thePersonclass. Modify theintroducemethod to include the species in its output. Print the species using both an instance (person_object.species) and the class name (Person.species). Simple Circle Class:Define a classCirclewith an__init__method that takes aradiusand stores it as an instance attribute. Add two methods:calculate_area(self)andcalculate_circumference(self)that return the area and circumference, respectively (you can use3.14159for pi or importmath.pi). Create aCircleobject and print its area and circumference.
Mini Project: Simple Book Class
Goal: Create a class to represent books in a small library or bookstore context.
Steps:
Define the Book Class:- Create a class named
Book. - Define the
__init__method. It should accepttitle,author, andisbn(International Standard Book Number) as arguments and store them as instance attributes (self.title,self.author,self.isbn). - Add another instance attribute
is_checked_outand initialize it toFalsewithin__init__.
- Create a class named
- Add Methods:
- Define an instance method
display_info(self)that prints the book’s title, author, and ISBN in a readable format. - Define an instance method
check_out(self)that:- Checks if
self.is_checked_outisFalse. - If it’s
False, setself.is_checked_outtoTrueand print a confirmation message like “[Title] has been checked out.” - If it’s already
True, print a message like “[Title] is already checked out.”
- Checks if
- Define an instance method
check_in(self)that:- Checks if
self.is_checked_outisTrue. - If it’s
True, setself.is_checked_outtoFalseand print a confirmation message like “[Title] has been checked in.” - If it’s already
False, print a message like “[Title] was not checked out.”
- Checks if
- Define an instance method
- Instantiate and Use:
- Create at least two different
Bookobjects with sample data. - Call
display_info()on both books. - Try checking out the first book using
check_out(). - Try checking out the first book again (it should say it’s already checked out).
- Call
display_info()on the first book again (to potentially see if status changed, though we aren’t printing it here). - Check in the first book using
check_in(). - Try checking in the second book (which wasn’t checked out).
- Create at least two different
dditional Sources:


