Python OOPs: Objects and Classes Complete Guide
Master Python Object-Oriented Programming with focus on objects, classes, abstraction, and encapsulation. Learn OOP fundamentals with practical examples.
Objects
Real-world entities
Classes
Blueprints for objects
Abstraction
Hide complexity
Encapsulation
Data protection
What is Object-Oriented Programming?
Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around objects rather than functions and logic. Python is a multi-paradigm language that fully supports OOP concepts.
Key OOP Concepts
- Class: Blueprint for creating objects
- Object: Instance of a class
- Attribute: Data stored in object/class
- Method: Function defined in class
- Constructor: Special method for initialization
- Self: Reference to current instance
OOP Pillars (We'll Cover)
- Abstraction: Hide complex implementation details
- Encapsulation: Bundle data with methods
- Inheritance: Create new classes from existing
- Polymorphism: Same interface, different implementation
Note: This tutorial focuses on Abstraction & Encapsulation
Real-world Analogy
Think of a class as a blueprint for a house. The object is the actual house built from that blueprint. Each house (object) has attributes (rooms, color) and methods (openDoor, turnOnLights).
# Defining a simple class
class Dog:
"""A simple Dog class"""
# Class attribute (shared by all instances)
species = "Canis familiaris"
# Constructor method (initializer)
def __init__(self, name, age):
# Instance attributes (unique to each instance)
self.name = name
self.age = age
# Instance method
def bark(self):
return f"{self.name} says: Woof!"
def describe(self):
return f"{self.name} is {self.age} years old and is a {self.species}"
# Creating objects (instances) of Dog class
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
# Accessing attributes
print(f"Dog 1 name: {dog1.name}") # Buddy
print(f"Dog 2 age: {dog2.age}") # 5
print(f"Both are: {dog1.species}") # Canis familiaris
# Calling methods
print(dog1.bark()) # Buddy says: Woof!
print(dog2.describe()) # Max is 5 years old and is a Canis familiaris
# Checking object types
print(type(dog1)) #
print(isinstance(dog1, Dog)) # True
# Object identity (memory address)
print(f"dog1 id: {id(dog1)}")
print(f"dog2 id: {id(dog2)}")
Classes: Blueprints for Objects
A class is a blueprint or template for creating objects. It defines the attributes (data) and methods (functions) that the objects will have.
| Class Component | Description | Example | Access |
|---|---|---|---|
| Class Attribute | Shared by all instances of the class | species = "Animal" |
ClassName.attr or obj.attr |
| Instance Attribute | Unique to each object instance | self.name = name |
obj.attr only |
| Instance Method | Function that operates on instance | def bark(self): |
obj.method() |
| Class Method | Function that operates on class | @classmethod |
ClassName.method() |
| Static Method | Utility function in class namespace | @staticmethod |
ClassName.method() |
| Constructor | Initializes new objects | __init__(self) |
Called automatically |
| String Representation | String representation of object | __str__(self) |
str(obj) or print(obj) |
class BankAccount:
"""A Bank Account class demonstrating class components"""
# Class attribute (shared by all accounts)
bank_name = "Python Bank"
interest_rate = 0.05 # 5% annual interest
def __init__(self, account_holder, initial_balance=0):
"""Constructor - Initialize new account"""
# Instance attributes
self.account_holder = account_holder
self._balance = initial_balance # Private convention
self.account_number = self._generate_account_number()
# Track all accounts
if not hasattr(BankAccount, 'all_accounts'):
BankAccount.all_accounts = []
BankAccount.all_accounts.append(self)
def _generate_account_number(self):
"""Private method to generate account number"""
import random
return f"ACC{random.randint(10000, 99999)}"
# Instance method
def deposit(self, amount):
"""Deposit money into account"""
if amount > 0:
self._balance += amount
return f"Deposited ${amount}. New balance: ${self._balance}"
return "Deposit amount must be positive"
def withdraw(self, amount):
"""Withdraw money from account"""
if 0 < amount <= self._balance:
self._balance -= amount
return f"Withdrew ${amount}. New balance: ${self._balance}"
return "Insufficient funds or invalid amount"
def get_balance(self):
"""Get current balance"""
return f"Balance: ${self._balance}"
# Class method
@classmethod
def change_interest_rate(cls, new_rate):
"""Change interest rate for all accounts"""
cls.interest_rate = new_rate
return f"Interest rate changed to {new_rate*100}%"
@classmethod
def get_total_accounts(cls):
"""Get total number of accounts"""
return len(cls.all_accounts) if hasattr(cls, 'all_accounts') else 0
# Static method
@staticmethod
def calculate_interest(principal, years):
"""Calculate interest (utility function)"""
return principal * (1 + BankAccount.interest_rate) ** years - principal
# Special methods
def __str__(self):
"""String representation for users"""
return f"Account({self.account_number}): {self.account_holder} - Balance: ${self._balance}"
def __repr__(self):
"""String representation for developers"""
return f"BankAccount('{self.account_holder}', {self._balance})"
# Creating accounts
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print("=== Instance Methods ===")
print(account1.deposit(500)) # Deposited $500. New balance: $1500
print(account2.withdraw(200)) # Withdrew $200. New balance: $300
print("\n=== Class Methods ===")
print(f"Bank name: {BankAccount.bank_name}")
print(f"Total accounts: {BankAccount.get_total_accounts()}") # 2
print(BankAccount.change_interest_rate(0.06)) # Interest rate changed to 6.0%
print("\n=== Static Method ===")
interest = BankAccount.calculate_interest(1000, 2)
print(f"Interest on $1000 for 2 years: ${interest:.2f}")
print("\n=== Special Methods ===")
print(account1) # Uses __str__
print(repr(account2)) # Uses __repr__
print("\n=== Accessing Attributes ===")
print(f"Account holder: {account1.account_holder}")
print(f"Account number: {account1.account_number}")
# Class vs Instance attributes
print(f"\nClass attribute access:")
print(f"BankAccount.bank_name: {BankAccount.bank_name}")
print(f"account1.bank_name: {account1.bank_name}")
# Modifying class attribute affects all instances
BankAccount.bank_name = "Python OOP Bank"
print(f"\nAfter change - account1.bank_name: {account1.bank_name}")
print(f"After change - account2.bank_name: {account2.bank_name}")
self Parameter: In Python, self is a convention (not a keyword) that refers to the current instance. It must be the first parameter of instance methods. When you call obj.method(), Python automatically passes obj as self.
Objects: Instances of Classes
Objects are instances of classes. Each object has its own state (attribute values) but shares the same behavior (methods) defined in the class.
class Car:
"""Car class representing a vehicle"""
# Class attribute
wheels = 4
def __init__(self, brand, model, year, color):
# Instance attributes
self.brand = brand
self.model = model
self.year = year
self.color = color
self._mileage = 0 # Private by convention
self.is_running = False
def start(self):
"""Start the car"""
if not self.is_running:
self.is_running = True
return f"{self.brand} {self.model} started."
return f"{self.brand} {self.model} is already running."
def stop(self):
"""Stop the car"""
if self.is_running:
self.is_running = False
return f"{self.brand} {self.model} stopped."
return f"{self.brand} {self.model} is already stopped."
def drive(self, miles):
"""Drive the car for specified miles"""
if self.is_running:
self._mileage += miles
return f"Drove {miles} miles. Total mileage: {self._mileage}"
return "Cannot drive. Car is not running!"
def get_mileage(self):
"""Get current mileage"""
return self._mileage
def repaint(self, new_color):
"""Change car color"""
old_color = self.color
self.color = new_color
return f"Car repainted from {old_color} to {new_color}"
def __str__(self):
return f"{self.year} {self.brand} {self.model} ({self.color})"
# Creating multiple car objects
car1 = Car("Toyota", "Camry", 2022, "Blue")
car2 = Car("Honda", "Civic", 2023, "Red")
car3 = Car("Tesla", "Model 3", 2024, "White")
print("=== Car Objects ===")
print(f"Car 1: {car1}")
print(f"Car 2: {car2}")
print(f"Car 3: {car3}")
print("\n=== Operating Cars ===")
print(car1.start()) # Toyota Camry started.
print(car1.drive(50)) # Drove 50 miles. Total mileage: 50
print(car1.drive(30)) # Drove 30 miles. Total mileage: 80
print(car1.stop()) # Toyota Camry stopped.
print(f"\nCar1 mileage: {car1.get_mileage()}") # 80
print(f"Car2 mileage: {car2.get_mileage()}") # 0 (not driven yet)
print("\n=== Modifying Objects ===")
print(car2.repaint("Black")) # Car repainted from Red to Black
print(f"Car2 color: {car2.color}") # Black
print("\n=== Object Identity and Equality ===")
# Different objects with same values
car4 = Car("Toyota", "Camry", 2022, "Blue")
print(f"car1: {id(car1)}")
print(f"car4: {id(car4)}")
print(f"car1 == car4: {car1 == car4}") # False (different objects)
print(f"Same class? {type(car1) == type(car4)}") # True
print("\n=== Object Attributes ===")
# Accessing attributes
print(f"Car1 brand: {car1.brand}")
print(f"Car1 model: {car1.model}")
print(f"Car1 wheels: {car1.wheels}") # Class attribute
# Listing all attributes
print(f"\nCar1 attributes: {car1.__dict__}")
print(f"Car class attributes: {Car.__dict__.keys()}")
print("\n=== Dynamic Attributes ===")
# Python allows adding attributes dynamically
car1.vin_number = "1HGCM82633A123456"
print(f"Car1 VIN: {car1.vin_number}")
# But this only affects car1, not other cars or the class
try:
print(f"Car2 VIN: {car2.vin_number}")
except AttributeError:
print("Car2 doesn't have vin_number attribute")
print("\n=== Multiple Objects with Different States ===")
# Each object maintains its own state
cars = [car1, car2, car3, car4]
for i, car in enumerate(cars, 1):
print(f"Car{i}: {car.brand} {car.model}, Running: {car.is_running}, Mileage: {car.get_mileage()}")
Object Memory Model
Class Definition (Shared)
- Methods (functions)
- Class attributes
- Blueprint structure
Object Instance (Unique)
- Instance attributes
- Current state values
- Memory location
Key Insight: Multiple objects share the same class methods but have separate copies of instance attributes.
Abstraction: Hide Complexity
Abstraction is the concept of hiding complex implementation details and showing only the essential features of an object. It helps manage complexity by focusing on what an object does rather than how it does it.
Real-world Abstraction Example
When you drive a car, you use the steering wheel, pedals, and gear shift (interface) without needing to know how the engine, transmission, or braking system works internally (implementation).
# =============================================
# EXAMPLE 1: Email System Abstraction
# =============================================
class EmailSender:
"""Abstraction for sending emails - hides complexity"""
def __init__(self, smtp_server="smtp.gmail.com", port=587):
self.smtp_server = smtp_server
self.port = port
self._connection = None # Private - internal detail
def connect(self):
"""Connect to email server (hidden complexity)"""
# Simulate complex connection logic
print(f"[DEBUG] Connecting to {self.smtp_server}:{self.port}")
# Complex networking code would go here
self._connection = "CONNECTED"
print("[DEBUG] Connection established")
def _authenticate(self, username, password):
"""Private method - authentication details hidden"""
print(f"[DEBUG] Authenticating {username}")
# Complex authentication logic
return True
def _prepare_email(self, to, subject, body):
"""Private method - email preparation hidden"""
print(f"[DEBUG] Preparing email to {to}")
email_data = {
'to': to,
'subject': subject,
'body': body,
'headers': {'Content-Type': 'text/html'}
}
return email_data
def _send_raw_email(self, email_data):
"""Private method - actual sending hidden"""
print(f"[DEBUG] Sending email: {email_data['subject']}")
# Complex SMTP protocol handling
return True
# PUBLIC INTERFACE - What users interact with
def send_email(self, to, subject, body, username, password):
"""
Simple public method - hides all complexity
Users only need to know this interface
"""
# Step 1: Connect (complexity hidden)
self.connect()
# Step 2: Authenticate (complexity hidden)
if not self._authenticate(username, password):
return "Authentication failed"
# Step 3: Prepare email (complexity hidden)
email_data = self._prepare_email(to, subject, body)
# Step 4: Send (complexity hidden)
if self._send_raw_email(email_data):
return f"Email sent successfully to {to}"
else:
return "Failed to send email"
def disconnect(self):
"""Clean up connection"""
if self._connection:
print("[DEBUG] Disconnecting from server")
self._connection = None
# Using the abstraction
print("=== Email System Abstraction ===")
sender = EmailSender()
# User only needs to know this simple interface
result = sender.send_email(
to="client@example.com",
subject="Project Update",
body="Hello, here's the latest update...",
username="me@example.com",
password="password123"
)
print(f"\nResult: {result}")
sender.disconnect()
print("\n" + "="*50 + "\n")
# =============================================
# EXAMPLE 2: Payment Processor Abstraction
# =============================================
class PaymentProcessor:
"""Abstraction for payment processing"""
def process_payment(self, amount, payment_method):
"""
Simple public interface
Hides all the complex payment gateway logic
"""
# Validate input
if amount <= 0:
return "Invalid amount"
# Process based on payment method
if payment_method == "credit_card":
return self._process_credit_card(amount)
elif payment_method == "paypal":
return self._process_paypal(amount)
elif payment_method == "bank_transfer":
return self._process_bank_transfer(amount)
else:
return "Unsupported payment method"
# Private methods - implementation details hidden
def _process_credit_card(self, amount):
"""Complex credit card processing logic"""
print(f"[DEBUG] Processing ${amount} via credit card")
print("[DEBUG] Validating card details...")
print("[DEBUG] Charging card...")
print("[DEBUG] Updating transaction records...")
return f"Credit card payment of ${amount} processed successfully"
def _process_paypal(self, amount):
"""Complex PayPal processing logic"""
print(f"[DEBUG] Processing ${amount} via PayPal")
print("[DEBUG] Redirecting to PayPal...")
print("[DEBUG] Handling OAuth...")
print("[DEBUG] Completing transaction...")
return f"PayPal payment of ${amount} processed successfully"
def _process_bank_transfer(self, amount):
"""Complex bank transfer logic"""
print(f"[DEBUG] Processing ${amount} via bank transfer")
print("[DEBUG] Generating payment reference...")
print("[DEBUG] Creating bank transaction...")
print("[DEBUG] Sending confirmation...")
return f"Bank transfer of ${amount} initiated successfully"
# Using the payment abstraction
print("=== Payment Processor Abstraction ===")
processor = PaymentProcessor()
# Simple interface for users
payment_result = processor.process_payment(99.99, "credit_card")
print(f"\nPayment Result: {payment_result}")
print("\n" + "="*50 + "\n")
# =============================================
# EXAMPLE 3: File Compression Abstraction
# =============================================
class FileCompressor:
"""Abstraction for file compression - hides algorithm details"""
def compress(self, file_path, algorithm="zip"):
"""
Simple interface for file compression
Hides complex compression algorithms
"""
print(f"Compressing: {file_path}")
if algorithm == "zip":
return self._compress_zip(file_path)
elif algorithm == "gzip":
return self._compress_gzip(file_path)
elif algorithm == "bzip2":
return self._compress_bzip2(file_path)
else:
return "Unsupported compression algorithm"
# Private methods - algorithm details hidden
def _compress_zip(self, file_path):
"""Complex ZIP compression algorithm"""
print("[DEBUG] Using DEFLATE algorithm...")
print("[DEBUG] Creating archive structure...")
print("[DEBUG] Calculating CRC...")
return f"{file_path}.zip created successfully"
def _compress_gzip(self, file_path):
"""Complex GZIP compression algorithm"""
print("[DEBUG] Using LZ77 algorithm...")
print("[DEBUG] Adding gzip header...")
print("[DEBUG] Calculating Adler-32 checksum...")
return f"{file_path}.gz created successfully"
def _compress_bzip2(self, file_path):
"""Complex BZIP2 compression algorithm"""
print("[DEBUG] Using Burrows-Wheeler transform...")
print("[DEBUG] Applying Huffman coding...")
print("[DEBUG] Creating bzip2 header...")
return f"{file_path}.bz2 created successfully"
# Using the compression abstraction
print("=== File Compression Abstraction ===")
compressor = FileCompressor()
# Simple interface - user doesn't need to know algorithm details
result = compressor.compress("document.txt", algorithm="zip")
print(f"\n{result}")
# Try different algorithm
result2 = compressor.compress("data.csv", algorithm="gzip")
print(f"\n{result2}")
print("\n" + "="*50 + "\n")
# =============================================
# KEY TAKEAWAY: Benefits of Abstraction
# =============================================
print("=== Benefits of Abstraction ===")
print("1. Simplified Interface: Users interact with simple methods")
print("2. Implementation Hiding: Complex details are hidden")
print("3. Code Maintainability: Change implementation without affecting users")
print("4. Reduced Complexity: Users focus on 'what' not 'how'")
print("5. Reusability: Same interface can have multiple implementations")
_method_name are considered "protected" (for internal use), while methods starting with double underscore __method_name are "private" (name mangled). This helps implement abstraction by indicating which methods are part of the internal implementation.
Encapsulation: Data Protection
Encapsulation is the bundling of data (attributes) and methods that operate on that data within a single unit (class), while restricting direct access to some of the object's components. This is often called "information hiding".
Encapsulation in Practice
Think of a capsule medicine: the medicine (data) is protected inside the capsule (class), and you interact with it through specific methods (swallowing) without direct access to the contents.
# =============================================
# EXAMPLE 1: Bank Account with Encapsulation
# =============================================
class SecureBankAccount:
"""
Bank account with encapsulation
Direct access to balance is restricted
"""
def __init__(self, account_holder, initial_balance=0):
# Private attributes (by convention)
self._account_holder = account_holder
self.__balance = initial_balance # Name mangled for stronger privacy
self.__transaction_history = []
self.__pin = self._generate_pin()
# Record initial deposit
if initial_balance > 0:
self.__add_transaction("Initial deposit", initial_balance)
# PRIVATE METHODS (implementation details)
def _generate_pin(self):
"""Generate a secure PIN"""
import random
return str(random.randint(1000, 9999))
def __validate_amount(self, amount):
"""Validate amount is positive"""
return amount > 0
def __add_transaction(self, description, amount):
"""Add transaction to history"""
import datetime
transaction = {
'timestamp': datetime.datetime.now(),
'description': description,
'amount': amount,
'balance': self.__balance
}
self.__transaction_history.append(transaction)
def __verify_pin(self, provided_pin):
"""Verify PIN without exposing it"""
return self.__pin == provided_pin
# PUBLIC INTERFACE (controlled access)
def deposit(self, amount, pin):
"""Deposit money with PIN verification"""
if not self.__verify_pin(pin):
return "Invalid PIN"
if self.__validate_amount(amount):
self.__balance += amount
self.__add_transaction("Deposit", amount)
return f"Deposited ${amount}. New balance: ${self.__balance}"
return "Invalid deposit amount"
def withdraw(self, amount, pin):
"""Withdraw money with PIN verification"""
if not self.__verify_pin(pin):
return "Invalid PIN"
if not self.__validate_amount(amount):
return "Invalid withdrawal amount"
if amount > self.__balance:
return "Insufficient funds"
self.__balance -= amount
self.__add_transaction("Withdrawal", -amount)
return f"Withdrew ${amount}. New balance: ${self.__balance}"
def get_balance(self, pin):
"""Get balance with PIN verification"""
if not self.__verify_pin(pin):
return "Invalid PIN"
return f"Current balance: ${self.__balance}"
def get_statement(self, pin, last_n=5):
"""Get recent transactions"""
if not self.__verify_pin(pin):
return "Invalid PIN"
statement = f"Statement for {self._account_holder}\n"
statement += "=" * 40 + "\n"
recent = self.__transaction_history[-last_n:] if last_n > 0 else []
for trans in recent:
date = trans['timestamp'].strftime("%Y-%m-%d %H:%M")
amount = f"+${trans['amount']}" if trans['amount'] > 0 else f"-${-trans['amount']}"
statement += f"{date}: {trans['description']:20} {amount:>10} Balance: ${trans['balance']}\n"
statement += "=" * 40 + "\n"
statement += f"Current Balance: ${self.__balance}"
return statement
def change_pin(self, old_pin, new_pin):
"""Change PIN with verification"""
if not self.__verify_pin(old_pin):
return "Invalid current PIN"
if len(new_pin) != 4 or not new_pin.isdigit():
return "PIN must be 4 digits"
self.__pin = new_pin
return "PIN changed successfully"
print("=== Bank Account Encapsulation ===")
account = SecureBankAccount("John Doe", 1000)
# Try to access private attributes directly (won't work properly)
print(f"Account holder: {account._account_holder}") # Accessible but by convention shouldn't
try:
print(f"Balance: {account.__balance}") # This will fail
except AttributeError as e:
print(f"Cannot access __balance directly: {e}")
# Proper way to interact with the account
print("\n=== Using Public Interface ===")
print(account.deposit(500, "1234")) # Invalid PIN
print(account.deposit(500, account._SecureBankAccount__pin)) # Using actual PIN
print(account.withdraw(200, account._SecureBankAccount__pin))
print(account.get_balance(account._SecureBankAccount__pin))
print("\n=== Getting Statement ===")
print(account.get_statement(account._SecureBankAccount__pin, 3))
print("\n=== Trying to Change PIN ===")
print(account.change_pin("wrong", "9999")) # Should fail
print(account.change_pin(account._SecureBankAccount__pin, "9999")) # Should work
print("\n" + "="*50 + "\n")
# =============================================
# EXAMPLE 2: Temperature Converter with Validation
# =============================================
class Temperature:
"""
Temperature class with encapsulation
Ensures temperature stays in valid range
"""
ABSOLUTE_ZERO_C = -273.15 # Class constant
def __init__(self, celsius=0):
# Private attribute with validation
self.__celsius = self.__validate_celsius(celsius)
def __validate_celsius(self, value):
"""Private validation method"""
if value < self.ABSOLUTE_ZERO_C:
raise ValueError(f"Temperature cannot be below absolute zero ({self.ABSOLUTE_ZERO_C}°C)")
return value
# Getter methods (provide controlled access)
def get_celsius(self):
"""Get temperature in Celsius"""
return self.__celsius
def get_fahrenheit(self):
"""Get temperature in Fahrenheit"""
return (self.__celsius * 9/5) + 32
def get_kelvin(self):
"""Get temperature in Kelvin"""
return self.__celsius + 273.15
# Setter methods with validation
def set_celsius(self, value):
"""Set temperature in Celsius with validation"""
self.__celsius = self.__validate_celsius(value)
def set_fahrenheit(self, value):
"""Set temperature using Fahrenheit"""
celsius = (value - 32) * 5/9
self.__celsius = self.__validate_celsius(celsius)
def set_kelvin(self, value):
"""Set temperature using Kelvin"""
celsius = value - 273.15
self.__celsius = self.__validate_celsius(celsius)
# Operator overloading for natural syntax
def __add__(self, other):
"""Add temperatures"""
if isinstance(other, Temperature):
return Temperature(self.__celsius + other.__celsius)
return Temperature(self.__celsius + other)
def __str__(self):
return f"{self.__celsius:.1f}°C ({self.get_fahrenheit():.1f}°F)"
print("=== Temperature Encapsulation ===")
temp = Temperature(25)
print(f"Initial temperature: {temp}")
# Using getters
print(f"\nIn different units:")
print(f"Celsius: {temp.get_celsius()}°C")
print(f"Fahrenheit: {temp.get_fahrenheit()}°F")
print(f"Kelvin: {temp.get_kelvin()}K")
# Using setters with validation
print(f"\n=== Changing Temperature ===")
temp.set_fahrenheit(77)
print(f"After setting to 77°F: {temp}")
temp.set_celsius(100)
print(f"After setting to 100°C: {temp}")
# Try invalid temperature
try:
temp.set_celsius(-300) # Below absolute zero
except ValueError as e:
print(f"\nError: {e}")
# Using operator overloading
temp1 = Temperature(20)
temp2 = Temperature(30)
result = temp1 + temp2
print(f"\n{temp1} + {temp2} = {result}")
print("\n" + "="*50 + "\n")
# =============================================
# EXAMPLE 3: Employee Management with Encapsulation
# =============================================
class Employee:
"""
Employee class demonstrating encapsulation
with property decorators
"""
def __init__(self, name, position, salary):
self._name = name
self._position = position
self.__salary = salary # Private
self.__bonus = 0
self.__performance_rating = 1.0 # 1.0 to 5.0
# Property decorators for controlled access
@property
def name(self):
"""Get employee name"""
return self._name
@name.setter
def name(self, value):
"""Set employee name with validation"""
if not value or not isinstance(value, str):
raise ValueError("Name must be a non-empty string")
self._name = value
@property
def position(self):
"""Get position"""
return self._position
@position.setter
def position(self, value):
"""Set position with validation"""
valid_positions = ["Developer", "Manager", "Designer", "Analyst"]
if value not in valid_positions:
raise ValueError(f"Position must be one of: {valid_positions}")
self._position = value
# Salary access with business logic
@property
def salary(self):
"""Get total compensation (salary + bonus)"""
return self.__salary + self.__bonus
def set_salary(self, new_salary, approver_level):
"""Set salary with approval check"""
if approver_level < 3:
raise PermissionError("Only managers can change salaries")
if new_salary < 0:
raise ValueError("Salary cannot be negative")
self.__salary = new_salary
def give_bonus(self, amount, reason):
"""Give bonus with validation"""
if amount <= 0:
raise ValueError("Bonus must be positive")
if not reason:
raise ValueError("Must provide reason for bonus")
self.__bonus += amount
print(f"Bonus of ${amount} given for: {reason}")
# Performance rating with encapsulation
def set_performance_rating(self, rating, evaluator):
"""Set performance rating with validation"""
if evaluator.position != "Manager":
raise PermissionError("Only managers can set performance ratings")
if not 1.0 <= rating <= 5.0:
raise ValueError("Rating must be between 1.0 and 5.0")
self.__performance_rating = rating
# Auto-adjust bonus based on performance
self.__adjust_bonus_based_on_performance()
def __adjust_bonus_based_on_performance(self):
"""Private method: Adjust bonus based on performance"""
if self.__performance_rating >= 4.5:
self.__bonus += self.__salary * 0.1 # 10% bonus
elif self.__performance_rating >= 4.0:
self.__bonus += self.__salary * 0.05 # 5% bonus
def get_performance_summary(self):
"""Get performance summary (encapsulated data)"""
return {
'rating': self.__performance_rating,
'bonus': self.__bonus,
'total_compensation': self.salary
}
def __str__(self):
return f"{self._name} - {self._position} (Total comp: ${self.salary})"
print("=== Employee Encapsulation ===")
emp = Employee("Alice Smith", "Developer", 80000)
print(f"Employee: {emp}")
# Using properties
print(f"\nName: {emp.name}")
print(f"Position: {emp.position}")
# Try to change position with validation
try:
emp.position = "CEO" # Not in valid positions
except ValueError as e:
print(f"Cannot change position: {e}")
emp.position = "Manager" # Valid change
print(f"New position: {emp.position}")
# Salary access (with bonus included)
print(f"\nTotal compensation: ${emp.salary}")
# Give bonus
emp.give_bonus(5000, "Excellent Q4 performance")
print(f"After bonus - Total compensation: ${emp.salary}")
# Set performance rating (with validation)
try:
emp.set_performance_rating(4.8, emp) # emp is not a Manager
except PermissionError as e:
print(f"Cannot set rating: {e}")
# Create a manager to set rating
manager = Employee("Bob Johnson", "Manager", 100000)
emp.set_performance_rating(4.8, manager)
# Check performance summary
summary = emp.get_performance_summary()
print(f"\nPerformance Summary:")
print(f" Rating: {summary['rating']}/5.0")
print(f" Bonus: ${summary['bonus']}")
print(f" Total Compensation: ${summary['total_compensation']}")
print("\n" + "="*50 + "\n")
# =============================================
# BENEFITS OF ENCAPSULATION
# =============================================
print("=== Benefits of Encapsulation ===")
print("1. Data Protection: Prevent invalid state")
print("2. Controlled Access: Validation in setters")
print("3. Implementation Hiding: Change internals without affecting users")
print("4. Maintainability: Business logic centralized")
print("5. Flexibility: Can add logging/auditing in getters/setters")
@property, @attribute.setter, and @attribute.deleter decorators provide an elegant way to implement encapsulation. They allow you to use methods like attributes while hiding the implementation details.
_var is a convention (protected), and double underscore __var triggers name mangling (_ClassName__var). However, both can still be accessed if needed. True encapsulation in Python relies on convention and trust.
Real-world OOP Application
E-commerce Product System
Complete e-commerce product with abstraction and encapsulation:
class Product:
def __init__(self, name, price, category):
self._name = name
self._price = self._validate_price(price)
self._category = category
self._discount = 0
def _validate_price(self, price):
if price < 0:
raise ValueError("Price cannot be negative")
return price
@property
def final_price(self):
return self._price * (1 - self._discount)
def apply_discount(self, percentage):
if 0 <= percentage <= 1:
self._discount = percentage
else:
raise ValueError("Discount must be between 0 and 1")
Game Character System
Game character with encapsulated health and abstraction:
class GameCharacter:
def __init__(self, name, health=100):
self.name = name
self.__health = min(max(health, 0), 100)
def take_damage(self, amount):
self.__health = max(0, self.__health - amount)
return self.__health > 0
def heal(self, amount):
self.__health = min(100, self.__health + amount)
def get_health_status(self):
if self.__health > 70: return "Healthy"
if self.__health > 30: return "Injured"
return "Critical"
Library Management
Library book system with encapsulation:
class LibraryBook:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.__isbn = isbn
self.__is_checked_out = False
self.__due_date = None
def checkout(self, borrower, days=14):
if not self.__is_checked_out:
self.__is_checked_out = True
# Calculate due date logic
return True
return False
Vehicle Management
Vehicle system with abstraction:
class Vehicle:
def start_engine(self):
self._connect_fuel_system()
self._ignite_spark_plugs()
self._engage_starter()
return "Engine started"
def _connect_fuel_system(self):
# Complex fuel system logic
pass
def _ignite_spark_plugs(self):
# Ignition logic
pass
OOP Best Practices
- Use meaningful class names: Nouns that represent real-world entities
- Follow single responsibility: Each class should have one job
- Encapsulate by default: Start with private attributes, expose via methods
- Use properties for getters/setters: Pythonic way to encapsulate
- Document your classes: Use docstrings for class and methods
- Keep methods small: Each method should do one thing well
- Use abstraction layers: Hide complexity from users
- Validate in constructors: Ensure objects start in valid state
- Avoid God classes: Don't put everything in one class
- Follow naming conventions:
_private,__really_private
# =============================================
# BAD OOP PRACTICE
# =============================================
class BadUser:
"""Example of bad OOP practices"""
def __init__(self):
# Public attributes (no encapsulation)
self.name = ""
self.email = ""
self.password = "" # Password exposed!
self.age = 0
# Mixing responsibilities
self.db_connection = None # Database logic mixed with user
# Methods doing too much
def save_to_database_and_send_email(self):
"""Mixing responsibilities"""
# Database code
# Email sending code
# Validation code
pass
# No validation
def set_age(self, age):
self.age = age # Could be negative or 1000!
# =============================================
# GOOD OOP PRACTICE
# =============================================
class User:
"""Example of good OOP practices"""
def __init__(self, name, email, password, age):
# Validation in constructor
self._name = self._validate_name(name)
self._email = self._validate_email(email)
self.__password_hash = self._hash_password(password) # Encrypted
self._age = self._validate_age(age)
self.__created_at = datetime.datetime.now()
# Private validation methods
def _validate_name(self, name):
if not name or len(name.strip()) < 2:
raise ValueError("Name must be at least 2 characters")
return name.strip()
def _validate_email(self, email):
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, email):
raise ValueError("Invalid email format")
return email
def _validate_age(self, age):
if not 0 <= age <= 150:
raise ValueError("Age must be between 0 and 150")
return age
def _hash_password(self, password):
"""Hash password for security"""
import hashlib
return hashlib.sha256(password.encode()).hexdigest()
# Property getters
@property
def name(self):
return self._name
@property
def email(self):
return self._email
@property
def age(self):
return self._age
@property
def created_at(self):
return self.__created_at
# Property setters with validation
@name.setter
def name(self, value):
self._name = self._validate_name(value)
@email.setter
def email(self, value):
self._email = self._validate_email(value)
@age.setter
def age(self, value):
self._age = self._validate_age(value)
# Business logic methods
def verify_password(self, password):
"""Verify password without exposing hash"""
return self.__password_hash == self._hash_password(password)
def change_password(self, old_password, new_password):
"""Change password with verification"""
if not self.verify_password(old_password):
raise ValueError("Incorrect current password")
if len(new_password) < 8:
raise ValueError("Password must be at least 8 characters")
self.__password_hash = self._hash_password(new_password)
return "Password changed successfully"
def is_adult(self):
"""Business logic method"""
return self._age >= 18
def __str__(self):
return f"User: {self._name} ({self._email})"
def __repr__(self):
return f"User(name='{self._name}', email='{self._email}', age={self._age})"
print("=== Good vs Bad OOP Comparison ===")
print("\nBad OOP Issues:")
print("1. No encapsulation (password exposed)")
print("2. No validation (invalid states possible)")
print("3. Mixed responsibilities (database + email)")
print("4. Poor naming (vague method names)")
print("5. No abstraction (exposes implementation)")
print("\nGood OOP Benefits:")
print("1. Full encapsulation (private attributes)")
print("2. Validation in constructors/setters")
print("3. Single responsibility (User class only)")
print("4. Clear, descriptive method names")
print("5. Abstraction (hides password hashing)")
print("6. Properties for controlled access")
print("7. Business logic methods")
print("8. Proper string representations")
# Example usage of good OOP
try:
user = User("John Doe", "john@example.com", "secure123", 25)
print(f"\nCreated user: {user}")
print(f"Is adult? {user.is_adult()}")
print(f"Password correct? {user.verify_password('secure123')}")
# Try to change password
user.change_password("secure123", "newsecure456")
print("Password changed successfully")
except ValueError as e:
print(f"Error: {e}")
Python OOP Key Takeaways
- Class is a blueprint, Object is an instance
- Abstraction hides complex implementation details from users
- Encapsulation bundles data with methods and restricts direct access
- Use
_single_underscorefor protected attributes/methods (convention) - Use
__double_underscorefor name mangling (stronger privacy) @propertydecorators provide elegant getter/setter syntax- Always validate data in constructors and setters
- Each class should have a single responsibility
- Use docstrings to document classes and methods
- Python OOP follows conventions rather than strict enforcement