OOP in Python: Classes, Objects and Inheritance for Beginners — Python Course #8
Introduction: What Is OOP and Why Does It Matter

Welcome to part 8 of 10 of our Python course for beginners. Until now we have used functions and modules to organize our code. Now we are going to take an important leap: Object-Oriented Programming (OOP).
OOP is a way of thinking about and structuring your code based on objects. But do not worry, it is simpler than it sounds. Let us use an analogy:
Why is OOP important?
- Better code organization: groups related data and functions in one place
- Code reuse: you can create new classes based on ones that already exist
- Models the real world: you can represent things like users, products, vehicles
- It is a standard: almost all modern languages use OOP (Java, C#, JavaScript, etc.)
Create Your First Class with class
A class is created with the class keyword. By convention, class names are written in PascalCase (each word starts with a capital letter).
1# The simplest possible class
2class Dog:
3 pass # "pass" means "do nothing for now"
4
5# Create an object (an instance of the class)
6my_dog = Dog()
7print(my_dog) # <__main__.Dog object at 0x...>
8print(type(my_dog)) # <class '__main__.Dog'>
We just created an empty Dog class and a my_dog object. Now let us give it characteristics and behaviors.
Add attributes directly
1class Dog:
2 pass
3
4my_dog = Dog()
5
6# We can add attributes on the fly
7my_dog.name = "Rex"
8my_dog.breed = "Labrador"
9my_dog.age = 3
10
11print(f"{my_dog.name} is a {my_dog.breed}, {my_dog.age} years old")
12# Rex is a Labrador, 3 years old
This works, but it is not the correct way to do it. That is what the __init__ method is for.
The __init__ Method (Constructor): Initializing Attributes
The __init__ method is the constructor of the class. It runs automatically every time you create a new object. This is where you define the initial attributes.
1class Dog:
2 def __init__(self, name, breed, age):
3 self.name = name
4 self.breed = breed
5 self.age = age
6
7# Now when creating the object, we pass the data
8my_dog = Dog("Rex", "Labrador", 3)
9
10print(f"{my_dog.name} is a {my_dog.breed}, {my_dog.age} years old")
11# Rex is a Labrador, 3 years old
12
13# We can create many objects with the same class
14another_dog = Dog("Luna", "Golden Retriever", 5)
15print(f"{another_dog.name} is a {another_dog.breed}, {another_dog.age} years old")
16# Luna is a Golden Retriever, 5 years old
Let us break down what happened:
__init__runs automatically when you callDog("Rex", "Labrador", 3)selfis a reference to the object being created (we will explain this in depth in the next section)self.name = namesaves the value "Rex" as an attribute of the object
1class Dog:
2 def __init__(self, name, breed="Mixed", age=1):
3 self.name = name
4 self.breed = breed
5 self.age = age
6
7# Just with the name
8dog1 = Dog("Buddy")
9print(f"{dog1.name}, {dog1.breed}, {dog1.age} year old")
10# Buddy, Mixed, 1 year old
self: The Reference to the Current Object
The self parameter is probably what confuses beginners the most. But it is simple:
self is a reference to the object that is using the method. When you write self.name, you are saying "the name attribute of this particular object".
1class Cat:
2 def __init__(self, name):
3 self.name = name # self.name = attribute of the object
4 # name = parameter we passed
5
6 def introduce(self):
7 # Here self refers to the specific cat that calls the method
8 print(f"Meow, I am {self.name}")
9
10# Create two cats
11cat1 = Cat("Whiskers")
12cat2 = Cat("Fluffy")
13
14cat1.introduce() # Meow, I am Whiskers
15cat2.introduce() # Meow, I am Fluffy
When you call cat1.introduce(), Python automatically passes cat1 as self. That is why you do not need to write cat1.introduce(cat1).
self must always be the first parameter of all methods in a class. Python passes it automatically; you never write it when calling the method.
Methods: Functions Inside a Class
Methods are functions that belong to a class. They define what an object can do.
1class BankAccount:
2 def __init__(self, owner, balance=0):
3 self.owner = owner
4 self.balance = balance
5
6 def deposit(self, amount):
7 """Add money to the account."""
8 if amount > 0:
9 self.balance += amount
10 print(f"Deposited {amount}. New balance: {self.balance}")
11 else:
12 print("Amount must be positive")
13
14 def withdraw(self, amount):
15 """Withdraw money from the account."""
16 if amount > self.balance:
17 print(f"Insufficient funds. You have {self.balance}")
18 elif amount <= 0:
19 print("Amount must be positive")
20 else:
21 self.balance -= amount
22 print(f"Withdrew {amount}. New balance: {self.balance}")
23
24 def check_balance(self):
25 """Show the current balance."""
26 print(f"Owner: {self.owner}")
27 print(f"Current balance: {self.balance}")
28
29# Create an account and use it
30account = BankAccount("Ana Lopez", 1000)
31account.check_balance()
32# Owner: Ana Lopez
33# Current balance: $1000
34
35account.deposit(500)
36# Deposited $500. New balance: $1500
37
38account.withdraw(200)
39# Withdrew $200. New balance: $1300
40
41account.withdraw(5000)
42# Insufficient funds. You have $1300
Notice how each method uses self.balance to access and modify the balance of the specific object. If you create another account, it will have its own independent balance.
Inheritance: Create a Class from Another
Inheritance lets you create a new class that inherits all the attributes and methods from another class. It is like saying: "A Dog is an Animal, but with additional features".
1class Animal:
2 """Base (parent) class for all animals."""
3
4 def __init__(self, name, species):
5 self.name = name
6 self.species = species
7 self.energy = 100
8
9 def eat(self):
10 self.energy += 10
11 print(f"{self.name} is eating. Energy: {self.energy}")
12
13 def sleep(self):
14 self.energy += 20
15 print(f"{self.name} is sleeping. Energy: {self.energy}")
16
17 def info(self):
18 print(f"{self.name} ({self.species}) - Energy: {self.energy}")
19
20
21class Dog(Animal):
22 """Child class that inherits from Animal."""
23
24 def __init__(self, name, breed):
25 # super() calls the __init__ of the parent class (Animal)
26 super().__init__(name, species="Canine")
27 self.breed = breed
28
29 def bark(self):
30 self.energy -= 5
31 print(f"{self.name} says: Woof woof!")
32
33 def fetch_ball(self):
34 self.energy -= 15
35 print(f"{self.name} fetches the ball. Energy: {self.energy}")
36
37
38class Cat(Animal):
39 """Another child class that inherits from Animal."""
40
41 def __init__(self, name, color):
42 super().__init__(name, species="Feline")
43 self.color = color
44
45 def meow(self):
46 self.energy -= 3
47 print(f"{self.name} says: Meow!")
48
49 def purr(self):
50 print(f"{self.name} is purring... prrr")
51
52
53# Use the classes
54rex = Dog("Rex", "Labrador")
55rex.info() # Rex (Canine) - Energy: 100 (inherited from Animal)
56rex.eat() # Rex is eating. Energy: 110 (inherited from Animal)
57rex.bark() # Rex says: Woof woof! (Dog's own method)
58rex.fetch_ball() # Rex fetches the ball. Energy: 90
59
60whiskers = Cat("Whiskers", "orange")
61whiskers.info() # Whiskers (Feline) - Energy: 100
62whiskers.sleep() # Whiskers is sleeping. Energy: 120
63whiskers.meow() # Whiskers says: Meow!
64whiskers.purr() # Whiskers is purring... prrr
Key points about inheritance:
class Dog(Animal)— the parentheses indicate who it inherits fromsuper().__init__(...)— calls the parent constructor to initialize inherited attributes- The child class has everything from the parent plus its own attributes and methods
- The child class can override parent methods if it needs different behavior
Basic Encapsulation: Public and Private Attributes
Encapsulation is the idea of protecting an object's internal data so it cannot be accidentally modified from outside. In Python, this is done by convention (not by language restriction).
| Convention | Meaning | Example |
|---|---|---|
name | Public: anyone can access | self.name = "Rex" |
_name | Protected: should not be accessed from outside (convention) | self._balance = 1000 |
__name | Private: Python changes the name internally (name mangling) | self.__password = "123" |
1class User:
2 def __init__(self, name, email, password):
3 self.name = name # Public
4 self._email = email # Protected (by convention)
5 self.__password = password # Private (name mangling)
6
7 def verify_password(self, attempt):
8 """Verify the password without exposing the actual value."""
9 return self.__password == attempt
10
11 def show_info(self):
12 # Inside the class we CAN access private attributes
13 print(f"Name: {self.name}")
14 print(f"Email: {self._email}")
15 print(f"Password: {'*' * len(self.__password)}")
16
17
18user = User("Ana", "[email protected]", "myKey123")
19
20# Public: free access
21print(user.name) # Ana
22
23# Protected: works, but you SHOULD NOT use it outside the class
24print(user._email) # [email protected]
25
26# Private: gives an error if you try to access directly
27# print(user.__password) # AttributeError!
28
29# The correct way is to use a method
30print(user.verify_password("myKey123")) # True
31print(user.verify_password("wrongKey")) # False
Special Methods: __str__, __repr__, __len__
Python has special methods (also called "magic methods" or "dunder methods") that you can define to customize how your objects behave.
1class Product:
2 def __init__(self, name, price, stock):
3 self.name = name
4 self.price = price
5 self.stock = stock
6
7 def __str__(self):
8 """Called when you use print() or str()."""
9 return f"{self.name} - {self.price} ({self.stock} units)"
10
11 def __repr__(self):
12 """Called in the interactive console or when debugging."""
13 return f"Product('{self.name}', {self.price}, {self.stock})"
14
15 def __len__(self):
16 """Called when you use len()."""
17 return self.stock
18
19 def __eq__(self, other):
20 """Called when you compare with ==."""
21 if not isinstance(other, Product):
22 return False
23 return self.name == other.name and self.price == other.price
24
25
26# Without __str__, print would show something like: <__main__.Product object at 0x...>
27product = Product("Laptop", 999.99, 15)
28
29print(product) # Laptop - $999.99 (15 units) (__str__)
30print(repr(product)) # Product('Laptop', 999.99, 15) (__repr__)
31print(len(product)) # 15 (__len__)
32
33# Compare products
34p1 = Product("Mouse", 25.00, 100)
35p2 = Product("Mouse", 25.00, 50)
36p3 = Product("Keyboard", 45.00, 30)
37
38print(p1 == p2) # True (same name and price)
39print(p1 == p3) # False (different name)
The most common special methods:
| Method | Triggered by | Use |
|---|---|---|
__str__ | print(obj), str(obj) | Human-readable representation |
__repr__ | Interactive console, repr(obj) | Technical representation for developers |
__len__ | len(obj) | Return a length/size |
__eq__ | obj1 == obj2 | Compare equality |
__lt__ | obj1 < obj2 | Compare less than |
__add__ | obj1 + obj2 | Add objects |
Practical Example: Library Management System
Let us combine everything we learned in a complete example: a system to manage books and loans for a library.
1from datetime import date, timedelta
2
3
4class Book:
5 """Represents a book in the library."""
6
7 def __init__(self, title, author, isbn, copies=1):
8 self.title = title
9 self.author = author
10 self.isbn = isbn
11 self.copies = copies
12 self._available = copies
13
14 def is_available(self):
15 """Check if there are copies available."""
16 return self._available > 0
17
18 def lend(self):
19 """Lend a copy of the book."""
20 if self.is_available():
21 self._available -= 1
22 return True
23 return False
24
25 def return_book(self):
26 """Return a copy of the book."""
27 if self._available < self.copies:
28 self._available += 1
29 return True
30 return False
31
32 def __str__(self):
33 return f"'{self.title}' by {self.author} ({self._available}/{self.copies} available)"
34
35 def __repr__(self):
36 return f"Book('{self.title}', '{self.author}', '{self.isbn}')"
37
38
39class Member:
40 """Represents a library member."""
41
42 def __init__(self, name, member_id):
43 self.name = name
44 self.member_id = member_id
45 self.borrowed_books = []
46
47 def has_book(self, book):
48 """Check if the member has a borrowed book."""
49 return any(b["book"].isbn == book.isbn for b in self.borrowed_books)
50
51 def __str__(self):
52 return f"Member: {self.name} (ID: {self.member_id}) - {len(self.borrowed_books)} books borrowed"
53
54
55class Library:
56 """Library management system."""
57
58 def __init__(self, name):
59 self.name = name
60 self._books = []
61 self._members = []
62 self._history = []
63
64 def add_book(self, book):
65 """Add a book to the catalog."""
66 self._books.append(book)
67 print(f"Book added: {book.title}")
68
69 def register_member(self, member):
70 """Register a new member."""
71 self._members.append(member)
72 print(f"Member registered: {member.name}")
73
74 def lend_book(self, member, book):
75 """Lend a book to a member."""
76 if not book.is_available():
77 print(f"'{book.title}' is not available")
78 return False
79
80 if member.has_book(book):
81 print(f"{member.name} already has '{book.title}'")
82 return False
83
84 book.lend()
85 due_date = date.today() + timedelta(days=14)
86 loan = {
87 "book": book,
88 "loan_date": date.today(),
89 "due_date": due_date,
90 }
91 member.borrowed_books.append(loan)
92 self._history.append({
93 "type": "loan",
94 "member": member.name,
95 "book": book.title,
96 "date": date.today(),
97 })
98 print(f"Loan successful: '{book.title}' to {member.name}")
99 print(f" Return before: {due_date}")
100 return True
101
102 def return_book(self, member, book):
103 """Return a borrowed book."""
104 for i, loan in enumerate(member.borrowed_books):
105 if loan["book"].isbn == book.isbn:
106 book.return_book()
107 member.borrowed_books.pop(i)
108 self._history.append({
109 "type": "return",
110 "member": member.name,
111 "book": book.title,
112 "date": date.today(),
113 })
114 print(f"Return successful: '{book.title}' from {member.name}")
115 return True
116
117 print(f"{member.name} does not have '{book.title}'")
118 return False
119
120 def search_book(self, term):
121 """Search books by title or author."""
122 results = [
123 book for book in self._books
124 if term.lower() in book.title.lower()
125 or term.lower() in book.author.lower()
126 ]
127 return results
128
129 def show_catalog(self):
130 """Show all books in the catalog."""
131 print(f"\n=== {self.name} Catalog ===")
132 for book in self._books:
133 status = "Available" if book.is_available() else "Not available"
134 print(f" {book} - {status}")
135 print()
136
137
138# === Testing the system ===
139
140# Create the library
141library = Library("Central Library")
142
143# Add books
144book1 = Book("One Hundred Years of Solitude", "Gabriel Garcia Marquez", "978-0307474728", copies=3)
145book2 = Book("The Little Prince", "Antoine de Saint-Exupery", "978-0156012195", copies=2)
146book3 = Book("Don Quixote", "Miguel de Cervantes", "978-0060934347", copies=1)
147
148library.add_book(book1)
149library.add_book(book2)
150library.add_book(book3)
151
152# Register members
153member1 = Member("Maria Garcia", "M001")
154member2 = Member("Juan Lopez", "M002")
155
156library.register_member(member1)
157library.register_member(member2)
158
159# Show catalog
160library.show_catalog()
161
162# Lend books
163library.lend_book(member1, book1)
164library.lend_book(member2, book1)
165library.lend_book(member1, book2)
166
167# Check status
168library.show_catalog()
169print(member1)
170print(member2)
171
172# Return a book
173library.return_book(member1, book1)
174library.show_catalog()
175
176# Search books
177print("Search 'quixote':")
178for book in library.search_book("quixote"):
179 print(f" {book}")
_), and relationships between objects. In a real project you would also save the data in a database, but the class structure would be very similar.
Summary and Next Article
In this article you learned the fundamentals of Object-Oriented Programming in Python:
- Classes: blueprints for creating objects, defined with
class - __init__: the constructor that initializes the object's attributes
- self: the reference to the current object inside methods
- Methods: functions that belong to a class and define its behavior
- Inheritance: creating new classes based on existing classes
- Encapsulation: protecting internal data with the
_nameconvention - Special methods:
__str__,__repr__,__len__to customize behavior
In the next article (part 9 of 10) we will learn about file handling and exceptions: how to read and write files, handle errors gracefully with try/except, and create more robust programs. Do not miss it!
Comments
Sign in to leave a comment
No comments yet. Be the first!