Chapter 5: Object-Oriented Programming (OOP) in Python
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
5.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
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
dog1 = Dog("Rex", "German Shepherd")
print(dog1.bark()) # Output: Rex says Woof!
5.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()
)
5.3 Inheritance
Inheritance allows you to extend existing classes.
Example:
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.
5.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 Method | Purpose | Example |
---|---|---|
__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:
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"'{self.title}' by {self.author}"
def __eq__(self, other):
return self.title == other.title and self.author == other.author
def __repr__(self):
return f"Book(title='{self.title}', author='{self.author}')"
book = Book("1984", "George Orwell")
print(book) # Output: '1984' by George Orwell
print(repr(book)) # Output: Book(title='1984', author='George Orwell')
book2 = Book("1984", "George Orwell")
print(book == book2) # Output: True
5.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
File: task_manager.py
class Task:
def __init__(self, title, description=""):
self.title = title
self.description = description
self.completed = False
def mark_done(self):
self.completed = True
def __str__(self):
status = "Done" if self.completed else "Pending"
return f"[{status}] {self.title}{ ": " + self.description if len(self.description) > 0 else "" }"
class TaskManager:
def __init__(self):
self.tasks = []
def add_task(self, title, description=""):
task = Task(title, description)
self.tasks.append(task)
return task
def list_tasks(self):
return [str(task) for task in self.tasks]
def get_pending_tasks(self):
return [str(task) for task in self.tasks if not task.completed]
def get_completed_tasks(self):
return [str(task) for task in self.tasks if task.completed]
Usage Example
if __name__ == "__main__":
manager = TaskManager()
manager.add_task("Write unit tests", "For the task manager app")
manager.add_task("Refactor code")
manager.tasks[0].mark_done()
print("\nAll Tasks:")
print("\n".join(manager.list_tasks()))
print("\nPending Tasks:")
print(manager.get_pending_tasks())
Expected Output:
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 theTask
class if needed.
5.6 Writing Unit Tests for the Task Manager
File: test_task_manager.py
import pytest
from task_manager import TaskManager
@pytest.fixture
def manager():
return TaskManager()
def test_add_task(manager):
task = manager.add_task("Test Task", "Description")
assert len(manager.tasks) == 1
assert task.title == "Test Task"
assert task.description == "Description"
assert task.completed is False
def test_mark_task_done(manager):
task = manager.add_task("Complete me")
task.mark_done()
assert task.completed is True
def test_list_tasks(manager):
manager.add_task("Task 1")
manager.add_task("Task 2")
task_list = manager.list_tasks()
assert "[Pending] Task 1" in task_list[0]
assert "[Pending] Task 2" in task_list[1]
def test_filter_tasks(manager):
t1 = manager.add_task("Task 1")
t2 = manager.add_task("Task 2")
t1.mark_done()
pending = manager.get_pending_tasks()
completed = manager.get_completed_tasks()
assert t2 in pending
assert t1 in completed
Run Your Tests
pytest
Expected result:
collected 4 items
test_task_manager.py .... [100%]
4 passed in 0.03s
5.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