10xdev Book: Programming from Zero to Pro with Python
Chapter 10: Object-Oriented Programming (OOP) in Python
Chapter Introduction
So far, we’ve organized our code using functions, which is great for bundling actions. But what happens when your program needs to represent more complex things – like users, products, bank accounts, or characters in a game? Each of these “things” has its own data (like a name, balance, or health points) and its own actions (like logging in, depositing money, or attacking).
Trying to manage all this with separate variables and functions quickly becomes a tangled mess, especially in large projects. This is where Object-Oriented Programming (OOP) comes in.
OOP isn’t just a set of features; it’s a powerful way of thinking about your code. It lets you model real-world (or conceptual) entities as self-contained objects. Each object bundles its own data (called attributes) and the functions that operate on that data (called methods). It’s like creating mini-programs within your main program. 📦
This chapter introduces the core concepts of OOP in Python, helping you write more organized, modular, and scalable code.
1. The Big Idea: Classes and Objects
Imagine you’re designing cars. Before you build any actual cars, you need a blueprint that defines what a car is – it has wheels, an engine, a color, and it can start, stop, and accelerate.
- Class: This is the blueprint or template. In Python, we define a class using the
classkeyword. It describes the common structure and behavior for all objects of a certain kind (e.g., theCarblueprint). - Object (or Instance): This is the actual thing built from the blueprint. Each specific car you build (your red Toyota, my blue Ford) is an object or an instance of the
Carclass. Each object has its own specific data (red vs. blue) but shares the same behaviors (starting, stopping).
Defining a Simple Class:
Class names traditionally use PascalCase.
class Dog:
"""Represents a dog."""
# Class body - currently empty
pass # 'pass' is a placeholder for an empty block
2. Creating Objects: The __init__ Constructor and self
When we create an object from a class (a process called instantiation), we often need to set up its initial state (like the dog’s name and age). This is done using a special method called __init__.
__init__(self, ...)(The Constructor): This “magic” method (notice the double underscores) is automatically called every time you create a new object from the class. Its job is to initialize the object’s attributes.self: This is the most important concept here.selfis always the first parameter of any method inside a class. It represents the specific instance (object) being created or worked on. Python passes it automatically behind the scenes when you create an object or call its methods.
class Dog:
# The constructor method
def __init__(self, name: str, age: int):
"""Initializes a new Dog object."""
print(f"Creating a new dog named {name}...")
# Attributes are variables attached TO the specific object ('self')
self.name: str = name
self.age: int = age
# --- Instantiation: Creating objects from the class ---
# This automatically calls Dog.__init__(...)
my_dog = Dog("Buddy", 3)
your_dog = Dog("Lucy", 5)
# Accessing object attributes using dot notation
print(f"My dog's name is {my_dog.name}.") # Output: My dog's name is Buddy.
print(f"Your dog is {your_dog.age} years old.") # Output: Your dog is 5 years old.
Inside __init__, self.name = name means “attach the value of the name parameter to this specific dog object (self) as an attribute called name.”
3. Defining Behaviors: Methods
Methods are simply functions defined inside a class. They represent the actions an object can perform. They always receive self as their first parameter, allowing them to access and modify the object’s own attributes.
class Dog:
def __init__(self, name: str, age: int):
self.name: str = name
self.age: int = age
self.is_awake: bool = True # Dogs start awake
# A method representing an action
def bark(self):
"""Makes the dog bark."""
if self.is_awake:
print(f"{self.name} says: Woof!")
else:
print(f"{self.name} is sleeping...")
def sleep(self):
"""Puts the dog to sleep."""
if self.is_awake:
self.is_awake = False
print(f"{self.name} goes to sleep. Zzzz...")
else:
print(f"{self.name} is already sleeping.")
def wake_up(self):
"""Wakes the dog up."""
if not self.is_awake:
self.is_awake = True
print(f"{self.name} wakes up!")
else:
print(f"{self.name} is already awake.")
# Create a dog
buddy = Dog("Buddy", 3)
# Call methods on the object
buddy.bark() # Output: Buddy says: Woof!
buddy.sleep() # Output: Buddy goes to sleep. Zzzz...
buddy.bark() # Output: Buddy is sleeping...
buddy.wake_up() # Output: Buddy wakes up!
When you call buddy.bark(), Python secretly calls Dog.bark(buddy), passing the buddy object as self.
4. Class Attributes vs. Instance Attributes
- Instance Attributes: Defined inside
__init__usingself.attribute_name = value. These are unique to each object (likeself.name,self.age). - Class Attributes: Defined directly inside the
classblock, outside any method. These are shared by all instances of the class. Good for constants or properties common to all objects of that type.
class Dog:
# Class attribute - shared by all dogs
species: str = "Canis familiaris"
def __init__(self, name: str, age: int):
# Instance attributes - unique to each dog
self.name: str = name
self.age: int = age
dog1 = Dog("Rex", 2)
dog2 = Dog("Luna", 4)
print(f"{dog1.name} is a {dog1.species}") # Access via instance
print(f"{dog2.name} is also a {Dog.species}") # Access via class
5. User-Friendly Representation: __str__
If you just print(my_dog), you get an ugly default output like <__main__.Dog object at 0x...>. To provide a nice, human-readable string representation, define the __str__ magic method. It must return a string.
class Dog:
species: str = "Canis familiaris"
def __init__(self, name: str, age: int):
self.name: str = name
self.age: int = age
# Define the string representation
def __str__(self) -> str:
return f"{self.name} ({self.species}, {self.age} years old)"
buddy = Dog("Buddy", 3)
print(buddy) # Automatically calls buddy.__str__()
# Output: Buddy (Canis familiaris, 3 years old)
6. OOP Principles (Briefly)
OOP is built on a few core ideas:
- Encapsulation: Bundling data (attributes) and methods that operate on the data within a single unit (the object). This hides internal complexity.
- Abstraction: Hiding the complex implementation details and exposing only the necessary features (methods) of an object. Think of driving a car – you use the steering wheel and pedals (interface), you don’t need to know exactly how the engine works internally.
- Inheritance: (Not covered in detail here) Allowing a new class (child/subclass) to inherit attributes and methods from an existing class (parent/superclass), promoting code reuse.
- Polymorphism: (Not covered in detail here) Allowing objects of different classes to respond to the same method call in their own specific ways.
Python also supports Protocols (typing.Protocol), a way to define expected interfaces (like Exportable having to_csv and to_json methods) without requiring strict inheritance. This promotes decoupling, making functions work with any object that behaves correctly, regardless of its specific class.
7. Applied Project: Bank Account Class
Let’s build a simple BankAccount class.
# --- Bank Account Class ---
class BankAccount:
"""Represents a simple bank account."""
account_type: str = "Standard Checking" # Example class attribute
def __init__(self, owner_name: str, initial_balance: float = 0.0):
"""Initializes the bank account."""
if initial_balance < 0:
raise ValueError("Initial balance cannot be negative.") # Input validation
self.owner: str = owner_name
# Use a convention like _balance for attributes you might
# want to suggest are "internal", though Python doesn't enforce privacy.
self._balance: float = initial_balance
print(f"Account for {self.owner} created with balance: ${self._balance:.2f}")
def deposit(self, amount: float):
"""Adds funds to the account."""
if amount <= 0:
print("Error: Deposit amount must be positive.")
return # Exit the method early
self._balance += amount
print(f"Deposited ${amount:.2f}. New balance: ${self._balance:.2f}")
def withdraw(self, amount: float):
"""Removes funds from the account if sufficient funds exist."""
if amount <= 0:
print("Error: Withdrawal amount must be positive.")
return
if amount > self._balance:
print(f"Error: Insufficient funds. Cannot withdraw ${amount:.2f} (Balance: ${self._balance:.2f})")
return
self._balance -= amount
print(f"Withdrew ${amount:.2f}. New balance: ${self._balance:.2f}")
# Read-only access to balance using a method (or property - more advanced)
def get_balance(self) -> float:
"""Returns the current account balance."""
return self._balance
def __str__(self) -> str:
"""String representation of the account."""
return f"Account Owner: {self.owner}, Balance: ${self.get_balance():.2f}"
# --- Using the BankAccount Class ---
try:
account1 = BankAccount("Alice", 500.0)
account2 = BankAccount("Bob") # Uses default initial balance of 0.0
print("\n--- Transactions for Alice ---")
account1.deposit(250.50)
account1.withdraw(100.0)
account1.withdraw(800.0) # Should fail (insufficient funds)
account1.deposit(-50) # Should fail (negative deposit)
print("\n--- Account Summaries ---")
print(account1) # Uses __str__
print(account2)
# Accessing balance via the getter method
print(f"\nAlice's final balance: ${account1.get_balance():.2f}")
except ValueError as e:
print(f"Error creating account: {e}")
This example shows how OOP encapsulates the data (_balance) and the operations (deposit, withdraw) together, making the BankAccount object a self-contained unit.
8. Chapter Summary
- OOP models code around objects, bundling data (attributes) and behavior (methods).
- A Class is the blueprint; an Object is an instance created from it.
__init__(self, ...)is the constructor for initializing instance attributes.selfrefers to the specific object instance within its methods.- Instance attributes are unique per object; Class attributes are shared by all instances.
- Methods are functions defined in a class that act on an object’s data (via
self). __str__(self)provides a readable string representation forprint().- OOP helps create modular, reusable, and more maintainable code, especially for complex systems.
OOP provides a powerful way to structure your own code. But Python also comes packed with pre-built modules full of useful tools. Let’s explore the treasures within Python 3.14’s Standard Library.