Object-Oriented Programming

Chapter Outline

Chapter 8: Object-Oriented Programming (OOP)

As applications grow, you’ll need ways to organize code around data and behavior. That’s where Object-Oriented Programming (OOP) comes in.

In this chapter, we’ll cover:

  • Classes and Objects
  • Attributes and Methods
  • Inheritance
  • Dunder (Magic) Methods like __str__ and __repr__
  • A practical project: Task Manager class
  • Writing unit tests for class behaviors

8.1 Understanding Classes and Objects

What is a Class?

A class is a blueprint for creating objects that encapsulate data (attributes) and behavior (methods).

Simple Class Example

python
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
return f"{self.name} says Woof!"

This is the constructor method, called automatically when you create a new Dog object.

__init__ is a special method in Python (known as a dunder or "double underscore" method).

It takes three arguments:

  • self: A reference to the current object being created.
  • name: The name of the dog (e.g., "Buddy").
  • breed: The breed of the dog (e.g., "Labrador").

Creating an Object

python
dog1 = Dog("Rex", "German Shepherd")
print(dog1.bark()) # Output: Rex says Woof!

8.2 Attributes and Methods

  • Attributes = Variables associated with the object (self.name, self.breed)
  • Methods = Functions defined inside a class that operate on its data (bark())

8.3 Inheritance

Inheritance allows you to extend existing classes.

Example:

python
class Animal:
def speak(self):
return "Some sound"
class Cat(Animal):
def speak(self):
return "Meow"
cat = Cat()
print(cat.speak()) # Output: Meow

Here Cat is the child class, and Animal is the parent class.

8.4 Dunder Methods (__str__, __repr__)

Dunder methods (also called magic methods) are special methods that Python uses to enable the behavior of built-in operations such as:

  • Object creation
  • Arithmetic
  • Comparisons
  • String conversion
  • Iteration

They are always surrounded by double underscores, e.g., __init__, __str__, __len__.

These methods allow you to define how custom objects behave like built-in types.

Commonly Used Dunder Methods (With Examples)

Dunder MethodPurposeExample
__init__Constructor: initialize an object__init__(self)
__str__String representation (str(obj))__str__(self)
__repr__Developer representation (repr(obj))__repr__(self)
__len__Used by len(obj)__len__(self)
__getitem__Indexing: obj[index]__getitem__(self, index)
__setitem__Set value at index: obj[index] = x__setitem__(self, index, value)
__eq__, __lt__, etc.Comparisons: ==, <, etc.__eq__(self, other)
__add__, __mul__, etc.Arithmetic operations__add__(self, other)
__call__Make object callable like a function__call__(self, *args)
__enter__, __exit__Context managers (with block)__enter__(self), __exit__(self, ...)

Example:

python
1class Book:
2 def __init__(self, title, author):
3 self.title = title
4 self.author = author
5
6 def __str__(self):
7 return f"'{self.title}' by {self.author}"
8
9 def __eq__(self, other):
10 return self.title == other.title and self.author == other.author
11
12 def __repr__(self):
13 return f"Book(title='{self.title}', author='{self.author}')"
14
15book = Book("1984", "George Orwell")
16print(book) # Output: '1984' by George Orwell
17print(repr(book)) # Output: Book(title='1984', author='George Orwell')
18book2 = Book("1984", "George Orwell")
19print(book == book2) # Output: True

8.5 Practical Project: Task Manager Class

Let’s build a simple Task Manager where you can:

  • Add tasks
  • Mark tasks as done
  • List all tasks
  • Search tasks by status
task_manager.py
1class Task:
2 def __init__(self, title, description=""):
3 self.title = title
4 self.description = description
5 self.completed = False
6
7 def mark_done(self):
8 self.completed = True
9
10 def __str__(self):
11 status = "Done" if self.completed else "Pending"
12 return f"[{status}] {self.title}{ ": " + self.description if len(self.description) > 0 else "" }"
13
14class TaskManager:
15 def __init__(self):
16 self.tasks = []
17
18 def add_task(self, title, description=""):
19 task = Task(title, description)
20 self.tasks.append(task)
21 return task
22
23 def list_tasks(self):
24 return [str(task) for task in self.tasks]
25
26 def get_pending_tasks(self):
27 return [str(task) for task in self.tasks if not task.completed]
28
29 def get_completed_tasks(self):
30 return [str(task) for task in self.tasks if task.completed]

Usage Example

python
1if __name__ == "__main__":
2 manager = TaskManager()
3
4 manager.add_task("Write unit tests", "For the task manager app")
5 manager.add_task("Refactor code")
6
7 manager.tasks[0].mark_done()
8
9 print("\nAll Tasks:")
10 print("\n".join(manager.list_tasks()))
11
12 print("\nPending Tasks:")
13 print(manager.get_pending_tasks())

Expected Output:

bash
All Tasks:
[Done] Write unit tests: For the task manager application
[Pending] Refactor code
Pending Tasks:
[<__main__.Task object at 0x...>]

You can improve the pending tasks output by adding a__repr__ method to the Task class if needed.

8.6 Writing Unit Tests for the Task Manager

test_task_manager.py
1import pytest
2from task_manager import TaskManager
3
4@pytest.fixture
5def manager():
6 return TaskManager()
7
8def test_add_task(manager):
9 task = manager.add_task("Test Task", "Description")
10 assert len(manager.tasks) == 1
11 assert task.title == "Test Task"
12 assert task.description == "Description"
13 assert task.completed is False
14
15def test_mark_task_done(manager):
16 task = manager.add_task("Complete me")
17 task.mark_done()
18 assert task.completed is True
19
20def test_list_tasks(manager):
21 manager.add_task("Task 1")
22 manager.add_task("Task 2")
23 task_list = manager.list_tasks()
24 assert "[Pending] Task 1" in task_list[0]
25 assert "[Pending] Task 2" in task_list[1]
26
27def test_filter_tasks(manager):
28 t1 = manager.add_task("Task 1")
29 t2 = manager.add_task("Task 2")
30 t1.mark_done()
31
32 pending = manager.get_pending_tasks()
33 completed = manager.get_completed_tasks()
34
35 assert t2 in pending
36 assert t1 in completed

Run Your Tests

bash
pytest

Expected result:

bash
collected 4 items
test_task_manager.py .... [100%]
4 passed in 0.03s

8.7 Why This Matters for Web and API Development

OOP is essential when you start building:

  • Models in Django
  • Service layers in FastAPI
  • Domain logic in larger apps
  • Serializable objects for APIs

The Task Manager class you just built could easily become part of a REST API backend or a database model.

Conclusion

You’ve learned:

  • How to create classes and objects
  • How to work with attributes, methods, and inheritance
  • How to add dunder methods for better object representation
  • How to write and test OOP-style Python code

What is Next

In Chapter 6: Advanced Object-Oriented Programming in Python, we’ll build:

  • Inheritance and Polymorphism
  • Duck Typing and Dynamic Behavior
  • Abstract Base Classes (ABCs)
  • Composition vs. Inheritance

Check your understanding

Test your knowledge of Object-Oriented Programming (OOP) in Python

Feedback