14. Object-Oriented Programming - Basics
🚀 Unlock the power of Object-Oriented Programming! This post guides you through the core principles, from understanding classes and objects to mastering methods, variables, and encapsulation for building well-structured code. ✨
What we will learn in this post?
- 👉 Introduction to OOP
- 👉 Classes and Objects
- 👉 The __init__ Method (Constructor)
- 👉 Instance Variables and Methods
- 👉 Class Variables
- 👉 Class Methods and Static Methods
- 👉 Encapsulation and Name Mangling
- 👉 Conclusion!
Welcome to Object-Oriented Programming (OOP)! ✨
OOP organizes code around “objects” (like a Car or User)—digital versions of real-world things with their own data and actions. This makes software development intuitive, modular, and easier to manage than procedural programming, where code is just a sequence of instructions.
Why Choose OOP? 🤔
OOP offers clear advantages over traditional programming styles:
- Better Organization: Keeps code clean and modular.
- Reusability: Write code once, use it often, saving time.
- Easier Maintenance: Simple updates and debugging.
- Scalability: Great for developing complex, large-scale projects.
The Four Pillars of OOP 🏛️
OOP’s core principles are fundamental concepts that help build robust and maintainable software:
1. Encapsulation 🎁
Bundling an object’s data (attributes) and methods (behaviors) together, hiding internal complexities. Think of a remote control: you press buttons without seeing its inner workings.
2. Inheritance 👨👩👧👦
Allows a new class (child) to inherit properties and behaviors from an existing one (parent), promoting code reuse. For example, a SportsCar inherits general features from a Car.
3. Polymorphism 🎭
Meaning “many forms.” Objects of different classes can be treated as a common type. A Dog and Cat are both Animals that can makeSound(), but their sounds differ.
4. Abstraction 🖼️
Showing only essential details, hiding complex implementation. Using a smartphone, you interact with its interface, not its intricate internal hardware.
OOP Concept Diagram (Inheritance & Polymorphism Example)
%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#ff4f81','primaryTextColor':'#fff','primaryBorderColor':'#c43e3e','lineColor':'#e67e22','secondaryColor':'#6b5bff','tertiaryColor':'#ffd700'}}}%%
classDiagram
class Animal {
+makeSound()
}
class Dog {
+breed
+makeSound()
}
class Cat {
+purr()
+makeSound()
}
Animal <|-- Dog
Animal <|-- Cat
style Animal fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14
style Dog fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14
style Cat fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14
Understanding Classes & Objects 🏗️
Let’s demystify classes and objects in programming! Think of them as fundamental building blocks for organizing your code.
What is a Class? 🗺️
A class is like a blueprint or a template. It’s not a real thing itself, but a design for creating things.
- Imagine the design plans for a car. Those plans define what a car is (it has wheels, a color, a model) and what it can do (drive, brake). This design is your
Carclass. - It defines a set of attributes (data like
color,model) and methods (actions likedrive(),honk()).
What is an Object? 🚗
An object is a real “thing” built from a class’s blueprint. It’s an instance of a class.
- Following our car analogy, a specific red sedan parked outside, or a blue SUV driving down the street – these are objects.
- Each object has its own unique set of attribute values (e.g., one car is “Red”, another is “Blue”) but follows the same general design from the class.
The Relationship: Blueprint to Instance 🤝
The relationship is simple:
- A class is the template or blueprint.
- An object is a concrete instance created from that blueprint. You can create many distinct objects from a single class.
Visualizing the Relationship
graph LR
CLASS["🗺️ Class: Car Blueprint"]:::gold --> OBJ1["🚗 Object: My Red Sedan"]:::pink
CLASS --> OBJ2["🚙 Object: Your Blue SUV"]:::purple
CLASS --> OBJ3["🚗 Object: Green Hatchback"]:::teal
classDef gold fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef pink fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef purple fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef teal fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
class CLASS gold;
class OBJ1 pink;
class OBJ2 purple;
class OBJ3 teal;
linkStyle default stroke:#e67e22,stroke-width:3px;
Defining a Class in Python ✍️
To create a class, you use the class keyword, followed by the class name (conventionally, names are CamelCase).
Simple Example
Here’s how you define a basic class and create an object from it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Our Class Blueprint for a Pet
class Pet:
# A class attribute (shared by all instances unless overridden)
species = "Mammal"
# Creating an object (an instance) of the Pet class
my_dog = Pet()
your_cat = Pet()
# Accessing attributes of our objects
print(f"My pet's species: {my_dog.species}")
print(f"Your pet's species: {your_cat.species}")
# Output:
# My pet's species: Mammal
# Your pet's species: Mammal
In this example, Pet is our class, and my_dog and your_cat are two different objects created from that Pet class.
Practical Examples: Real-World Class Usage 💼
Below are practical, runnable examples showing how classes and objects are used in real applications:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# Example 1 — User Account Management (Web Applications)
class UserAccount:
def __init__(self, username, email):
self.username = username
self.email = email
self.is_active = True
def deactivate(self):
self.is_active = False
return f"Account {self.username} has been deactivated"
def update_email(self, new_email):
self.email = new_email
return f"Email updated to {new_email}"
# Creating user accounts
user1 = UserAccount("alice_dev", "alice@example.com")
user2 = UserAccount("bob_admin", "bob@example.com")
print(user1.username) # alice_dev
print(user1.update_email("alice.new@example.com"))
# Example 2 — Product Catalog (E-commerce)
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def is_available(self):
return self.stock > 0
def apply_discount(self, percentage):
self.price = self.price * (1 - percentage / 100)
return f"New price: ${self.price:.2f}"
laptop = Product("Gaming Laptop", 1200.00, 5)
print(f"{laptop.name}: ${laptop.price}")
print(laptop.apply_discount(10)) # New price: $1080.00
print(f"Available: {laptop.is_available()}") # True
# Example 3 — Temperature Sensor (IoT Applications)
class TemperatureSensor:
def __init__(self, sensor_id, location):
self.sensor_id = sensor_id
self.location = location
self.readings = []
def add_reading(self, temp):
self.readings.append(temp)
def get_average(self):
if not self.readings:
return 0
return sum(self.readings) / len(self.readings)
sensor = TemperatureSensor("TEMP001", "Living Room")
sensor.add_reading(22.5)
sensor.add_reading(23.0)
sensor.add_reading(22.8)
print(f"Average temp: {sensor.get_average():.1f}°C") # 22.8°C
Further Resources 📚
- For more detailed information, check out the official Python Classes tutorial. –>
Meet __init__: Your Object’s First Hello! 👋
The __init__ (pronounced “dunder init”) method in Python is like a special welcome committee for new objects. Think of it as your object’s constructor. It’s automatically called every time you create a new object from a class, setting things up right from the start.
The Magic Behind Object Creation ✨
When you write my_object = MyClass(values), Python immediately looks for __init__ inside MyClass. Its job is to initialize the new, empty object with its starting characteristics or attributes. This ensures every object begins with the properties it needs.
Understanding self 🎯
The self parameter is always the first parameter in __init__ (and other methods within a class). It’s a special reference to the current object being created. Using self.attribute_name = value assigns a value to an attribute specifically for that particular object, making each instance unique.
Initializing Attributes: Giving Objects Their Traits 🎨
Inside __init__, you define what properties your object will have. For example, a Dog object might get a self.name and self.breed. These attributes are set using the values passed when you create the object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Dog:
def __init__(self, name, breed):
# 'self' refers to the new Dog object being created.
# It allows us to set attributes specific to THIS dog.
self.name = name # Assigns the given 'name' to this dog's 'name' attribute.
self.breed = breed # Assigns the given 'breed' to this dog's 'breed' attribute.
# --- Examples of creating objects with different initialization values ---
# Creating the first Dog object
my_dog = Dog("Buddy", "Golden Retriever")
# Output:
# my_dog is now an object of type Dog.
# It has a 'name' attribute set to "Buddy".
# It has a 'breed' attribute set to "Golden Retriever".
# Creating a second Dog object with different values
your_dog = Dog("Lucy", "Beagle")
# Output:
# your_dog is also an object of type Dog, separate from my_dog.
# Its 'name' attribute is "Lucy".
# Its 'breed' attribute is "Beagle".
# Accessing attributes of the created objects
print(f"My dog is {my_dog.name} ({my_dog.breed}).")
# Output: My dog is Buddy (Golden Retriever).
print(f"Your dog is {your_dog.name} ({your_dog.breed}).")
# Output: Your dog is Lucy (Beagle).
How Object Creation Works (Simplified Flow) ⚙️
graph TD
START["🎯 Call: Dog('Buddy', 'Golden Retriever')"]:::pink --> CREATE["⚙️ Python Creates Empty Dog Object"]:::gold
CREATE --> INIT["🔧 Python Calls __init__"]:::purple
INIT --> NAME["📝 Set self.name = 'Buddy'"]:::teal
NAME --> BREED["📝 Set self.breed = 'Golden Retriever'"]:::teal
BREED --> RETURN["✅ Initialized Object Returned"]:::green
RETURN --> STORE["📦 Variable my_dog Holds Object"]:::orange
classDef pink fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef gold fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef purple fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef teal fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef green fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef orange fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
class START pink;
class CREATE gold;
class INIT purple;
class NAME teal;
class BREED teal;
class RETURN green;
class STORE orange;
linkStyle default stroke:#e67e22,stroke-width:3px;
Understanding Objects: Your Digital Pals! 🥳
What are Instance Variables (Attributes)? 📊
Think of instance variables as unique facts or characteristics for each specific object created from a class. If you have a Car class, each car object (e.g., my_car, your_car) will have its own color, make, or speed. They are personal to that object! You define them inside the __init__ method using self.variable_name.
What are Instance Methods? 🛠️
Instance methods are actions or behaviors that a specific object can perform. These methods can access and modify the object’s own instance variables using the special self keyword. For example, a Car object might have a drive() method that uses its speed attribute.
The Magical self Keyword ✨
self is a reference to the current instance of the class. When you call my_car.display_info(), self inside display_info() refers to my_car. It’s how an object talks about itself and its own data.
Let’s See It in Action! 🚗💨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# First, define our blueprint: The Car Class
class Car:
def __init__(self, make, model, color):
# These are instance variables (attributes)
self.make = make # Each car has its own make
self.model = model # Each car has its own model
self.color = color # Each car has its own color
def display_info(self, owner="Someone"):
# This is an instance method
# It uses 'self' to access THIS CAR's specific attributes
return f"{owner}'s car: a {self.color} {self.make} {self.model}."
# Now, let's create different car objects! Each is a unique 'instance'.
my_car = Car("Toyota", "Camry", "blue")
your_car = Car("Honda", "Civic", "red")
friends_car = Car("Ford", "Focus", "green")
# Calling instance methods on specific objects.
# Each object uses its OWN instance variables to produce its unique output.
print(my_car.display_info("My")) # Output: My car: a blue Toyota Camry.
print(your_car.display_info("Your")) # Output: Your car: a red Honda Civic.
print(friends_car.display_info("Friend's")) # Output: Friend's car: a green Ford Focus.
# Notice how each car object has its own unique information, thanks to instance variables!
Visualizing Objects 🖼️
%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#ff4f81','primaryTextColor':'#fff','primaryBorderColor':'#c43e3e','lineColor':'#e67e22','secondaryColor':'#6b5bff','tertiaryColor':'#ffd700'}}}%%
classDiagram
class Car {
-str make
-str model
-str color
+__init__(make, model, color)
+display_info(owner) str
}
Car --|> my_car
Car --|> your_car
Car --|> friends_car
my_car : make = "Toyota"
my_car : model = "Camry"
my_car : color = "blue"
your_car : make = "Honda"
your_car : model = "Civic"
your_car : color = "red"
friends_car : make = "Ford"
friends_car : model = "Focus"
friends_car : color = "green"
style Car fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14
style my_car fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14
style your_car fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14
style friends_car fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14
Understanding Class & Instance Variables 🤝
Let’s demystify how classes store data in Python! It’s all about what’s shared and what’s unique.
What’s the Difference? 🧐
- Class Variables: Imagine a notepad everyone in a group shares. They are declared directly inside the class but outside any method. Every object of that class shares one single copy of this variable. Change it once, and everyone sees the update!
- Instance Variables: Now, picture each person having their own private notebook. These are declared inside methods (usually
__init__usingself.). Each object gets its own unique copy. Changing it for one object doesn’t affect others.
When to Use Them? 🤔
- Class Variables: Perfect for data that should be consistent across all objects – like a default setting (e.g.,
Car.WHEELS_COUNT = 4), or a counter tracking how many objects have been created. - Instance Variables: Ideal for data that each object needs to store independently – like a
car.color,person.name, oraccount.balance.
How to Access? 🚀
- Class Variables: Use
ClassName.variable(e.g.,Car.WHEELS_COUNT) orinstance.variable. - Instance Variables: Use
instance.variable(e.g.,my_car.color).
Example Time! 🛠️
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Car:
WHEELS_COUNT = 4 # This is a Class Variable (shared!)
def __init__(self, color, model):
self.color = color # This is an Instance Variable (unique!)
self.model = model # This is an Instance Variable (unique!)
# Create car objects
car1 = Car("Red", "Sedan")
car2 = Car("Blue", "SUV")
print(f"Car 1: {car1.color} {car1.model} with {car1.WHEELS_COUNT} wheels.")
print(f"Car 2: {car2.color} {car2.model} with {car2.WHEELS_COUNT} wheels.")
# Let's change the class variable directly via the class
Car.WHEELS_COUNT = 6
print("\nAfter changing Car.WHEELS_COUNT to 6:")
print(f"Car 1 now has {car1.WHEELS_COUNT} wheels.") # Both cars reflect the change!
print(f"Car 2 now has {car2.WHEELS_COUNT} wheels.")
# Changing an instance variable only affects that instance
car1.color = "Green"
print(f"\nCar 1's new color: {car1.color}")
print(f"Car 2's color remains: {car2.color}")
Click to see a visual concept!
```mermaid %%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#ff4f81','primaryTextColor':'#07610eff','primaryBorderColor':'#c43e3e','lineColor':'#e67e22','secondaryColor':'#6b5bff','tertiaryColor':'#ffd700'}}}%% classDiagram class Car { +int WHEELS_COUNT = 4 +string color +string model +__init__(color, model) } Car --|> car1 : has Car --|> car2 : has note for Car "WHEELS_COUNT is shared by ALL Car instances" note for car1 "color and model are unique to car1" note for car2 "color and model are unique to car2" style Car fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14 style car1 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14 style car2 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14 ```@classmethod vs. @staticmethod: Unlocking Class Power! 🧑💻
Ever wondered how to add special behaviors to your Python classes beyond regular methods? @classmethod and @staticmethod are your friends! They let you define methods that belong to the class itself, rather than a specific instance.
@classmethod Explained: The Class’s Helper 🏫
A @classmethod takes the class itself as its first argument, traditionally named cls. This means it can access or modify class-level attributes, or even create new instances in different ways (like an alternative constructor).
1
2
3
4
5
6
7
8
9
10
11
12
13
class Product:
discount_rate = 0.10 # A class attribute
def __init__(self, name, price):
self.name = name
self.price = price
@classmethod
def from_string(cls, product_str):
# 'cls' here refers to the Product class
name, price = product_str.split('-')
return cls(name, float(price)) # Creates an instance of 'cls' (Product)
item = Product.from_string("Laptop-1200.00")
print(f"Product: {item.name}, Price: ${item.price:.2f}")
When to use: For alternative constructors or methods needing access to class attributes or the class itself.
@staticmethod Explained: The Independent Utility ⚙️
A @staticmethod doesn’t take self (the instance) or cls (the class) as its first argument. It’s just a regular function nested inside a class. It has no access to instance or class-specific data, making it useful for utility functions that logically belong to the class but don’t depend on its state.
1
2
3
4
5
6
7
8
9
10
11
12
class MathHelper:
@staticmethod
def add_numbers(a, b):
return a + b
@staticmethod
def is_positive(number):
return number > 0
result = MathHelper.add_numbers(5, 3)
print(f"5 + 3 = {result}")
print(f"Is -10 positive? {MathHelper.is_positive(-10)}")
When to use: For utility functions that don’t need access to self or cls, like formatters, validators, or simple calculations.
Key Differences & When to Choose 💡
| Feature | @classmethod | @staticmethod |
|---|---|---|
| First Argument | cls (the class) | None |
| Accesses | Class attributes/methods | Neither instance nor class attributes |
| Use Case | Alternative constructors, class-level logic | Utility functions, helpers |
Here’s a quick decision guide:
graph TD
START["🔧 Method inside a class?"]:::pink --> INSTANCE{"🤔 Needs instance data (self)?"}:::gold
INSTANCE -- "Yes" --> REGULAR["✅ Regular Method"]:::green
INSTANCE -- "No" --> CLASS{"🎯 Needs class data (cls)?"}:::gold
CLASS -- "Yes" --> CLASSMETHOD["📋 @classmethod"]:::purple
CLASS -- "No" --> STATICMETHOD["⚙️ @staticmethod"]:::teal
classDef pink fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef gold fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef purple fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef teal fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef green fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
class START pink;
class INSTANCE gold;
class REGULAR green;
class CLASS gold;
class CLASSMETHOD purple;
class STATICMETHOD teal;
linkStyle default stroke:#e67e22,stroke-width:3px;
For more detailed info, check out this great resource: Python @classmethod and @staticmethod explained
This clear distinction helps you write organized and efficient Python code! Happy coding! ✨
Encapsulation in Python: Your Data’s Keeper! 📦🔒
Encapsulation is a core OOP concept that bundles data (attributes) and the methods that operate on that data within a single unit, like a class. It helps control access to an object’s internal state, preventing unintended modifications and promoting data integrity. Python achieves encapsulation through conventions, not strict keywords.
🕵️♀️ Public vs. “Private” (The Underscore Way)
Python doesn’t have strict “private” access modifiers. Instead, it uses naming conventions to suggest whether an attribute is intended for internal use:
- Public Attributes: Attributes without a leading underscore (
attribute) are considered public and can be accessed directly from anywhere. - “Private” (Convention): A single leading underscore (
_attribute) is a convention signaling that the attribute is intended for internal use within the class or module. While technically accessible from outside, it’s a strong hint to developers not to touch it directly.
1
2
3
4
5
6
7
8
class User:
def __init__(self, username, email):
self.username = username # Public attribute
self._email = email # "Protected" convention, meant for internal use
my_user = User("Alice", "alice@example.com")
print(f"Username: {my_user.username}") # Output: Username: Alice
print(f"Email (discouraged access): {my_user._email}") # Output: Email (discouraged access): alice@example.com
📛 Name Mangling with Double Underscores (__)
Using two leading underscores (__attribute) triggers Python’s name mangling. This means Python internally renames the attribute to _ClassName__attribute to prevent naming conflicts in subclasses. It’s not true privacy, but it makes direct access from outside the class harder and discourages it.
1
2
3
4
5
6
7
8
9
10
11
class Vault:
def __init__(self, secret_code):
self.__secret_code = secret_code # Python renames to _Vault__secret_code
def get_code(self):
return self.__secret_code
safe = Vault("Python123")
# print(safe.__secret_code) # ERROR: AttributeError: 'Vault' object has no attribute '__secret_code'
print(f"Via method: {safe.get_code()}") # Output: Via method: Python123
print(f"Via mangled name: {safe._Vault__secret_code}") # Output: Via mangled name: Python123 (Still accessible, but very explicit)
✨ Property Decorators: Smart Attribute Control
The @property decorator offers a Pythonic way to manage attribute access. It allows you to define “getter,” “setter,” and “deleter” methods that behave like regular attributes, providing controlled access and allowing validation logic without changing how users interact with the attribute.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Circle:
def __init__(self, radius):
self._radius = 0 # Initialize with a safe value
self.radius = radius # Use the setter for initial validation
@property # Getter method for 'radius'
def radius(self):
return self._radius
@radius.setter # Setter method for 'radius'
def radius(self, value):
if not isinstance(value, (int, float)) or value < 0:
raise ValueError("Radius must be a non-negative number!")
self._radius = value
my_circle = Circle(10)
print(f"Circle radius: {my_circle.radius}") # Output: Circle radius: 10 (Uses the getter)
my_circle.radius = 15 # Uses the setter with validation
print(f"New radius: {my_circle.radius}") # Output: New radius: 15
# my_circle.radius = -5 # Raises ValueError: Radius must be a non-negative number!
Practical Examples: Encapsulation in Action 💼
Below are practical examples showing how encapsulation protects data and ensures validity in real applications:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# Example 1 — Bank Account with Protected Balance (Financial Applications)
class BankAccount:
def __init__(self, account_number, initial_balance=0):
self.account_number = account_number
self.__balance = initial_balance # Private attribute
@property
def balance(self):
"""Safe read-only access to balance"""
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return f"Deposited ${amount}. New balance: ${self.__balance}"
return "Invalid deposit amount"
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return f"Withdrew ${amount}. New balance: ${self.__balance}"
return "Insufficient funds or invalid amount"
account = BankAccount("ACC001", 1000)
print(account.balance) # 1000 (read-only access)
print(account.deposit(500)) # Deposited $500. New balance: $1500
print(account.withdraw(200)) # Withdrew $200. New balance: $1300
# account.__balance = 99999 # Won't affect the actual balance (name mangling)
# Example 2 — Email Validator with Property Setter (User Registration)
class User:
def __init__(self, username, email):
self.username = username
self._email = None
self.email = email # Triggers validation
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if '@' in value and '.' in value:
self._email = value
else:
raise ValueError("Invalid email format!")
def get_info(self):
return f"User: {self.username}, Email: {self._email}"
user = User("john_doe", "john@example.com")
print(user.get_info()) # User: john_doe, Email: john@example.com
user.email = "john.new@example.com" # Validation passes
# user.email = "invalid-email" # Raises ValueError: Invalid email format!
# Example 3 — Configuration Manager with Read-Only Settings (Application Config)
class AppConfig:
def __init__(self, api_key, max_retries):
self.__api_key = api_key # Private - security sensitive
self.__max_retries = max_retries
@property
def max_retries(self):
"""Public read access"""
return self.__max_retries
def get_masked_key(self):
"""Provide masked version of API key"""
return f"{self.__api_key[:4]}{'*' * (len(self.__api_key) - 4)}"
config = AppConfig("sk_live_abc123xyz789", 3)
print(config.get_masked_key()) # sk_l****************
print(f"Max retries: {config.max_retries}") # 3
# print(config.__api_key) # AttributeError - properly protected
Python’s Encapsulation Flow (Mermaid)
graph TD
START["📋 Class Definition"]:::pink --> CONVENTION{"🔑 Attribute Naming Convention"}:::gold
CONVENTION -- "No underscore" --> PUBLIC["🌐 Public: Direct Access"]:::green
CONVENTION -- "Single _ (protected)" --> PROTECTED["🛡️ Protected: Internal Use"]:::teal
CONVENTION -- "Double __ (private)" --> PRIVATE["🔒 Private: Name Mangled"]:::purple
PRIVATE --> MANGLED["📝 Accessible via _ClassName__attribute"]:::orange
MANGLED --> PROPERTY{"✨ Use @property?"}:::gold
PROPERTY --> CONTROL["🎯 Controlled Access with Validation"]:::green
classDef pink fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef gold fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef purple fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef teal fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef orange fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef green fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
class START pink;
class CONVENTION gold;
class PUBLIC green;
class PROTECTED teal;
class PRIVATE purple;
class MANGLED orange;
class PROPERTY gold;
class CONTROL green;
linkStyle default stroke:#e67e22,stroke-width:3px;
🎯 Hands-On Assignment
💡 Project: Library Management System (Click to expand)
🚀 Your Challenge:
Build a Library Management System using Object-Oriented Programming principles. Your system should manage books, members, and borrowing transactions with proper encapsulation and class design. 📚✨
📋 Requirements:
Part 1: Book Class
- Create a
Bookclass with attributes:title,author,isbn,available(boolean) - Use
__init__method to initialize all attributes - Implement instance methods:
display_info()- shows book detailsborrow()- marks book as unavailablereturn_book()- marks book as available
- Add encapsulation: make
_isbnprotected (single underscore) - Use
@propertydecorator for controlled access to ISBN
Part 2: Member Class
- Create a
Memberclass with attributes:name,member_id,borrowed_books(list) - Add class variable
total_membersto track member count - Implement methods:
borrow_book(book)- adds book to member's listreturn_book(book)- removes book from listdisplay_borrowed()- shows borrowed books
- Use
@classmethodto get total members count
Part 3: Library Class
- Create a
Libraryclass to manage books and members - Attributes:
name,books(list),members(list) - Methods:
add_book(book)- adds book to libraryregister_member(member)- registers new memberlend_book(isbn, member_id)- handles borrowingreceive_book(isbn, member_id)- handles returnsdisplay_available_books()- lists available books
💡 Implementation Hints:
- Use
selfto access instance variables and methods 🎯 - Remember that
__init__is called automatically when creating objects ⚙️ - Use class variables (
total_members) for shared data across instances 🔢 - Apply encapsulation with single underscore for protected attributes 🔐
- Use
@propertydecorator for getter methods 📊 - Validate operations (e.g., can't borrow unavailable books) ✅
- Use meaningful method names that describe actions 📝
Example Input/Output:
# Creating books
book1 = Book("Python Crash Course", "Eric Matthes", "978-1593279288")
book2 = Book("Clean Code", "Robert Martin", "978-0132350884")
# Creating library
city_library = Library("City Central Library")
city_library.add_book(book1)
city_library.add_book(book2)
# Creating members
member1 = Member("Alice", "M001")
member2 = Member("Bob", "M002")
city_library.register_member(member1)
city_library.register_member(member2)
# Borrowing books
city_library.lend_book("978-1593279288", "M001")
# Display available books
city_library.display_available_books()
# Output:
# Available Books:
# - Clean Code by Robert Martin (ISBN: 978-0132350884)
# Check member's borrowed books
member1.display_borrowed()
# Output:
# Alice's Borrowed Books:
# - Python Crash Course by Eric Matthes
# Display total members
print(f"Total members: {Member.get_total_members()}")
# Output: Total members: 2
🌟 Bonus Challenges:
- Add a
Magazineclass that inherits fromBookwith additionalissue_numberattribute 📰 - Implement
__str__and__repr__methods for better object representation 🎭 - Add private attributes using double underscore (
__attribute) with name mangling 🔒 - Create a
@staticmethodfor validating ISBN format 📖 - Add borrowing limits (e.g., max 3 books per member) using property setters 🚦
- Implement a late fee calculator using class methods 💰
- Add search functionality (by title, author, or ISBN) 🔍
- Create a reservation system for borrowed books 📅
Submission Guidelines:
- Test all classes with multiple objects to ensure independence 🧪
- Include examples of encapsulation (public, protected, private) 🔐
- Demonstrate the use of
@property,@classmethod, and@staticmethod⚡ - Show proper error handling (e.g., borrowing unavailable books) ⚠️
- Share your complete code in the comments 📝
- Explain your design decisions and class relationships 💭
- Include sample output showing all features working 📊
Share Your Solution! 💬
Completed the project? Post your code in the comments below! Show us your OOP mastery and creative implementation! 🎨
Conclusion
And there we have it! I truly hope you enjoyed today’s post and found something valuable to take away. Now it’s your turn to shine! ✨ What are your thoughts, experiences, or even burning questions on this topic? Don’t hold back – I’d absolutely love to read all your comments, feedback, and brilliant suggestions below. Let’s keep the conversation going! 👇💬