Python OOP Tutorial: Classes, Objects, and Inheritance for Beginners

Master Python object-oriented programming with practical examples. Learn classes, objects, inheritance, encapsulation, and polymorphism to build scalable machine learning systems and clean code

๐Ÿ“… Published: June 18, 2025 โœ๏ธ Updated: July 22, 2025 By Ojaswi Athghara
#python #oop #classes #inheritance #encapsulation

Python OOP Tutorial: Classes, Objects, and Inheritance for Beginners

When OOP Finally Clicked for Me

I'll never forget the moment Object-Oriented Programming (OOP) made sense. I was building my third machine learning project, and my code looked like this:

# 50+ global variables
model_name = "RandomForest"
learning_rate = 0.001
batch_size = 32
epochs = 100
weights = [0.5, 0.3, 0.2]
# ... 45 more variables ...

Every time I wanted to train a second model, I had to create model_name_2, learning_rate_2, and so on. It was a nightmare. Then my mentor showed me classes:

class Model:
    def __init__(self, name, learning_rate, batch_size):
        self.name = name
        self.learning_rate = learning_rate
        self.batch_size = batch_size
    
model1 = Model("RandomForest", 0.001, 32)
model2 = Model("NeuralNet", 0.01, 64)

Mind. Blown. ๐Ÿคฏ

I could create as many models as I wanted, each with their own properties, without variable name collisions. That's when OOP clickedโ€”it's about organizing code around objects, not scattered variables.

In this beginner-friendly guide, I'll teach you Python OOP from scratch, with practical examples that show why it matters for data science and machine learning.

Why Learn OOP as a Beginner?

Before diving into syntax, let's understand why OOP is essential.

Real-World Analogy

Think of OOP like blueprints and houses:

  • Class = Blueprint (defines what a house should have)
  • Object = Actual house (built from the blueprint)
  • Attributes = Properties (color, size, rooms)
  • Methods = Actions (open_door, turn_on_lights)

Just like one blueprint can create many houses, one class can create many objects!

Why ML Engineers Use OOP

Without OOP (messy):

# Model 1
model1_name = "RandomForest"
model1_accuracy = 0.85
model1_trained = False

# Model 2
model2_name = "SVM"
model2_accuracy = 0.92
model2_trained = True

# Functions that need to know which model
def train_model1():
    pass

def train_model2():
    pass

With OOP (clean):

class MLModel:
    def __init__(self, name):
        self.name = name
        self.accuracy = 0.0
        self.trained = False
    
    def train(self):
        print(f"Training {self.name}...")
        self.trained = True

# Create multiple models easily
model1 = MLModel("RandomForest")
model2 = MLModel("SVM")

model1.train()
model2.train()

Benefits:

  • โœ… Organization - Related data and functions stay together
  • โœ… Reusability - Create multiple instances from one class
  • โœ… Maintainability - Changes in one place affect all instances
  • โœ… Scalability - Easy to extend and add features

Classes and Objects: The Foundation

Let's start with the simplest possible class.

Creating Your First Class

class Person:
    """A simple class representing a person."""
    pass

# Create an object (instance) of the class
person1 = Person()
person2 = Person()

print(type(person1))  # <class '__main__.Person'>
print(type(person2))  # <class '__main__.Person'>

Key concepts:

  • class Person: - Defines a new class named Person
  • pass - Empty class body (placeholder)
  • person1 = Person() - Creates an object (instance) from the class
  • Each object is independent - changing person1 doesn't affect person2

Adding Attributes (Properties)

Attributes store data about an object:

class Person:
    """Person with attributes."""
    pass

# Add attributes to objects
person1 = Person()
person1.name = "Alice"
person1.age = 25

person2 = Person()
person2.name = "Bob"
person2.age = 30

print(f"{person1.name} is {person1.age} years old")  # Alice is 25 years old
print(f"{person2.name} is {person2.age} years old")  # Bob is 30 years old

But this is tedious! We need a better way...

The init Method: Object Initialization

The __init__ method is a constructorโ€”it runs automatically when you create an object.

Basic Constructor

class Person:
    """Person with constructor."""
    
    def __init__(self, name, age):
        """Initialize person with name and age."""
        self.name = name  # Instance attribute
        self.age = age    # Instance attribute

# Now creating a person is much cleaner!
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

print(f"{person1.name} is {person1.age}")  # Alice is 25
print(f"{person2.name} is {person2.age}")  # Bob is 30

Understanding self:

  • self refers to the specific instance being created
  • It's like saying "this person" or "this object"
  • Python passes it automatically (you don't include it when calling)

ML Example: Model Class

class MLModel:
    """Machine learning model with configuration."""
    
    def __init__(self, name, learning_rate=0.001, epochs=100):
        """
        Initialize ML model.
        
        Args:
            name (str): Model name
            learning_rate (float): Learning rate (default: 0.001)
            epochs (int): Training epochs (default: 100)
        """
        self.name = name
        self.learning_rate = learning_rate
        self.epochs = epochs
        self.trained = False  # Default state
        self.accuracy = 0.0   # Default accuracy
    
# Create different models with different configurations
model1 = MLModel("RandomForest", learning_rate=0.01, epochs=50)
model2 = MLModel("NeuralNetwork", learning_rate=0.001, epochs=200)

print(f"{model1.name}: lr={model1.learning_rate}, epochs={model1.epochs}")
print(f"{model2.name}: lr={model2.learning_rate}, epochs={model2.epochs}")

Methods: Adding Behavior

Methods are functions inside a class that define what objects can do.

Instance Methods

class Person:
    """Person with behavior."""
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def greet(self):
        """Instance method to greet."""
        return f"Hello, I'm {self.name} and I'm {self.age} years old."
    
    def have_birthday(self):
        """Increase age by 1."""
        self.age += 1
        print(f"Happy birthday! {self.name} is now {self.age}!")

# Use the methods
person = Person("Alice", 25)
print(person.greet())  # Hello, I'm Alice and I'm 25 years old.

person.have_birthday()  # Happy birthday! Alice is now 26!
print(person.greet())  # Hello, I'm Alice and I'm 26 years old.

ML Example: Complete Model Class

class MLModel:
    """Complete ML model with training and prediction."""
    
    def __init__(self, name, learning_rate=0.001):
        self.name = name
        self.learning_rate = learning_rate
        self.trained = False
        self.weights = []
    
    def train(self, X, y):
        """Train the model on data."""
        print(f"Training {self.name}...")
        print(f"Learning rate: {self.learning_rate}")
        print(f"Training samples: {len(X)}")
        
        # Simulate training
        self.weights = [0.5, 0.3, 0.2]  # Dummy weights
        self.trained = True
        
        print(f"โœ… {self.name} trained successfully!")
    
    def predict(self, X):
        """Make predictions."""
        if not self.trained:
            raise ValueError("Model must be trained before predicting!")
        
        print(f"Making predictions with {self.name}...")
        return [1, 0, 1, 0, 1]  # Dummy predictions
    
    def evaluate(self, X, y):
        """Evaluate model performance."""
        if not self.trained:
            raise ValueError("Model must be trained before evaluation!")
        
        predictions = self.predict(X)
        # Calculate accuracy (dummy)
        accuracy = 0.85
        print(f"Accuracy: {accuracy:.1%}")
        return accuracy

# Use the complete model
model = MLModel("RandomForest", learning_rate=0.01)

# Training data (dummy)
X_train = [[1, 2], [3, 4], [5, 6]]
y_train = [0, 1, 0]

# Train
model.train(X_train, y_train)

# Predict
predictions = model.predict([[7, 8]])
print(f"Predictions: {predictions}")

# Evaluate
accuracy = model.evaluate(X_train, y_train)

Class vs Instance Attributes

There are two types of attributes: class attributes (shared by all instances) and instance attributes (unique to each instance).

Understanding the Difference

class DataScientist:
    """Data scientist with shared and individual attributes."""
    
    # Class attribute (shared by all instances)
    profession = "Data Scientist"
    company = "TechCorp"
    
    def __init__(self, name, experience):
        # Instance attributes (unique to each instance)
        self.name = name
        self.experience = experience

# Create instances
ds1 = DataScientist("Alice", 3)
ds2 = DataScientist("Bob", 5)

# Access class attribute (same for all)
print(f"{ds1.name}'s profession: {ds1.profession}")  # Data Scientist
print(f"{ds2.name}'s profession: {ds2.profession}")  # Data Scientist

# Access instance attribute (unique to each)
print(f"{ds1.name} has {ds1.experience} years experience")  # 3 years
print(f"{ds2.name} has {ds2.experience} years experience")  # 5 years

# Change class attribute (affects all instances)
DataScientist.company = "NewTech"
print(f"{ds1.name} works at: {ds1.company}")  # NewTech
print(f"{ds2.name} works at: {ds2.company}")  # NewTech

# Change instance attribute (affects only that instance)
ds1.experience = 4
print(f"{ds1.name}: {ds1.experience} years")  # 4 years
print(f"{ds2.name}: {ds2.experience} years")  # 5 years (unchanged)

When to use each:

  • Class attributes: For data shared by all instances (constants, defaults)
  • Instance attributes: For data unique to each instance (name, age, properties)

Inheritance: Creating Class Hierarchies

Inheritance allows a class to inherit attributes and methods from another class. This is crucial for code reuse!

Basic Inheritance

# Parent class (Base class)
class Animal:
    """Base animal class."""
    
    def __init__(self, name, species):
        self.name = name
        self.species = species
    
    def make_sound(self):
        return "Some generic sound"
    
    def info(self):
        return f"{self.name} is a {self.species}"

# Child class (inherits from Animal)
class Dog(Animal):
    """Dog inherits from Animal."""
    
    def __init__(self, name, breed):
        # Call parent constructor
        super().__init__(name, "Dog")
        self.breed = breed
    
    # Override parent method
    def make_sound(self):
        return "Woof! Woof!"
    
    # Add new method (only for dogs)
    def fetch(self):
        return f"{self.name} is fetching the ball!"

# Create instances
generic_animal = Animal("Generic", "Unknown")
dog = Dog("Buddy", "Golden Retriever")

# Dog inherited methods from Animal
print(dog.info())  # Buddy is a Dog
print(dog.make_sound())  # Woof! Woof! (overridden)

# Dog-specific method
print(dog.fetch())  # Buddy is fetching the ball!

Key concepts:

  • class Dog(Animal): - Dog inherits from Animal
  • super().__init__() - Calls parent class constructor
  • Override - Child class redefines parent method
  • Extend - Child class adds new methods

ML Example: Model Inheritance

# Base ML Model class
class BaseMLModel:
    """Base class for all ML models."""
    
    def __init__(self, name):
        self.name = name
        self.trained = False
    
    def train(self, X, y):
        """Base training method."""
        print(f"Training {self.name}...")
        self.trained = True
    
    def predict(self, X):
        """Base prediction method."""
        if not self.trained:
            raise ValueError("Model must be trained first!")
        return []

# Linear Regression Model (inherits from BaseMLModel)
class LinearRegression(BaseMLModel):
    """Linear regression model."""
    
    def __init__(self, learning_rate=0.01):
        super().__init__("Linear Regression")
        self.learning_rate = learning_rate
        self.weights = None
        self.bias = None
    
    def train(self, X, y):
        """Override training for linear regression."""
        super().train(X, y)  # Call base training
        print(f"Learning rate: {self.learning_rate}")
        # Simulate weight calculation
        self.weights = [0.5, 0.3]
        self.bias = 0.1
        print("โœ… Weights calculated!")
    
    def predict(self, X):
        """Linear regression prediction."""
        super().predict(X)  # Check if trained
        print(f"Using weights: {self.weights}, bias: {self.bias}")
        return [1.5, 2.3, 3.1]  # Dummy predictions

# Neural Network Model (also inherits from BaseMLModel)
class NeuralNetwork(BaseMLModel):
    """Neural network model."""
    
    def __init__(self, layers, activation="relu"):
        super().__init__("Neural Network")
        self.layers = layers
        self.activation = activation
        self.model_weights = []
    
    def train(self, X, y):
        """Override training for neural network."""
        super().train(X, y)
        print(f"Architecture: {self.layers}")
        print(f"Activation: {self.activation}")
        self.model_weights = [[0.1, 0.2], [0.3, 0.4]]
        print("โœ… Network trained!")
    
    def predict(self, X):
        """Neural network prediction."""
        super().predict(X)
        print(f"Forward pass through {len(self.layers)} layers")
        return [0, 1, 1, 0]  # Dummy classifications

# Test inheritance
lr_model = LinearRegression(learning_rate=0.001)
nn_model = NeuralNetwork(layers=[64, 32, 16], activation="relu")

# Both models share base functionality but have unique behavior
lr_model.train([[1, 2], [3, 4]], [5, 6])
print(f"LR Predictions: {lr_model.predict([[7, 8]])}")

print("\n" + "="*50 + "\n")

nn_model.train([[1, 2], [3, 4]], [0, 1])
print(f"NN Predictions: {nn_model.predict([[7, 8]])}")

Encapsulation: Protecting Data

Encapsulation means hiding internal details and exposing only what's necessary. Python uses naming conventions for this.

Public, Protected, and Private

class BankAccount:
    """Bank account with encapsulation."""
    
    def __init__(self, owner, balance):
        self.owner = owner           # Public (anyone can access)
        self._balance = balance      # Protected (convention: internal use)
        self.__pin = 1234           # Private (name mangling)
    
    def deposit(self, amount):
        """Public method to deposit money."""
        if amount > 0:
            self._balance += amount
            print(f"Deposited ${amount}. New balance: ${self._balance}")
        else:
            print("Invalid amount!")
    
    def withdraw(self, amount, pin):
        """Public method to withdraw money."""
        if pin != self.__pin:
            print("โŒ Wrong PIN!")
            return
        
        if amount > self._balance:
            print("โŒ Insufficient funds!")
            return
        
        self._balance -= amount
        print(f"Withdrew ${amount}. New balance: ${self._balance}")
    
    def get_balance(self):
        """Public method to check balance."""
        return self._balance

# Test encapsulation
account = BankAccount("Alice", 1000)

# Public access (OK)
print(f"Owner: {account.owner}")  # Alice

# Protected access (possible but not recommended)
print(f"Balance: {account._balance}")  # 1000

# Private access (will fail!)
try:
    print(account.__pin)
except AttributeError as e:
    print(f"Error: {e}")  # Can't access private attribute

# Use public methods
account.deposit(500)  # Deposited $500. New balance: $1500
account.withdraw(200, 1234)  # Withdrew $200. New balance: $1300
account.withdraw(200, 9999)  # โŒ Wrong PIN!

Naming conventions:

  • self.public - Anyone can access
  • self._protected - Convention says "internal use only" (but accessible)
  • self.__private - Name mangling makes it hard to access from outside

Properties: Controlled Access

Use @property to add validation when getting/setting attributes:

class Temperature:
    """Temperature with validation."""
    
    def __init__(self, celsius):
        self._celsius = None
        self.celsius = celsius  # Trigger validation
    
    @property  # Getter
    def celsius(self):
        """Get temperature in Celsius."""
        return self._celsius
    
    @celsius.setter  # Setter
    def celsius(self, value):
        """Set temperature with validation."""
        if value < -273.15:
            raise ValueError("Temperature below absolute zero!")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        """Get temperature in Fahrenheit (computed)."""
        return self.celsius * 9/5 + 32

# Test properties
temp = Temperature(25)
print(f"Celsius: {temp.celsius}ยฐC")  # 25ยฐC
print(f"Fahrenheit: {temp.fahrenheit}ยฐF")  # 77.0ยฐF

# Change temperature
temp.celsius = 30
print(f"New temp: {temp.celsius}ยฐC, {temp.fahrenheit}ยฐF")

# Invalid temperature (will raise error)
try:
    temp.celsius = -300
except ValueError as e:
    print(f"Error: {e}")  # Temperature below absolute zero!

Polymorphism: Same Interface, Different Behavior

Polymorphism means different classes can use the same method names with different implementations.

Method Overriding

class Shape:
    """Base shape class."""
    
    def area(self):
        """Calculate area (to be overridden)."""
        return 0
    
    def describe(self):
        """Describe the shape."""
        return f"I'm a shape with area {self.area()}"

class Rectangle(Shape):
    """Rectangle shape."""
    
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        """Calculate rectangle area."""
        return self.width * self.height

class Circle(Shape):
    """Circle shape."""
    
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        """Calculate circle area."""
        return 3.14159 * self.radius ** 2

# Polymorphism in action
shapes = [
    Rectangle(5, 3),
    Circle(4),
    Rectangle(10, 2)
]

# Same method name, different behavior!
for shape in shapes:
    print(shape.describe())
# Output:
# I'm a shape with area 15
# I'm a shape with area 50.26544
# I'm a shape with area 20

ML Example: Different Model Types

class BasePredictor:
    """Base predictor class."""
    
    def fit(self, X, y):
        """Train the model."""
        raise NotImplementedError("Subclasses must implement fit()")
    
    def predict(self, X):
        """Make predictions."""
        raise NotImplementedError("Subclasses must implement predict()")

class KNNPredictor(BasePredictor):
    """K-Nearest Neighbors predictor."""
    
    def __init__(self, k=3):
        self.k = k
    
    def fit(self, X, y):
        print(f"KNN: Storing training data with k={self.k}")
        self.X_train = X
        self.y_train = y
    
    def predict(self, X):
        print(f"KNN: Finding {self.k} nearest neighbors")
        return [1, 0, 1]  # Dummy predictions

class DecisionTreePredictor(BasePredictor):
    """Decision tree predictor."""
    
    def __init__(self, max_depth=5):
        self.max_depth = max_depth
    
    def fit(self, X, y):
        print(f"Decision Tree: Building tree with max_depth={self.max_depth}")
        self.tree = "TreeStructure"  # Dummy tree
    
    def predict(self, X):
        print("Decision Tree: Traversing tree")
        return [0, 1, 0]  # Dummy predictions

# Polymorphism: same interface, different implementations
models = [
    KNNPredictor(k=5),
    DecisionTreePredictor(max_depth=10)
]

# Same methods work on all models!
X_train = [[1, 2], [3, 4]]
y_train = [0, 1]
X_test = [[5, 6]]

for model in models:
    model.fit(X_train, y_train)
    predictions = model.predict(X_test)
    print(f"Predictions: {predictions}\n")

Special Methods (Dunder Methods)

Special methods (also called "magic methods") start and end with double underscores (__). They define how objects behave with built-in operations.

Common Special Methods

class Book:
    """Book with special methods."""
    
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages
    
    def __str__(self):
        """Human-readable string representation."""
        return f"{self.title} ({self.pages} pages)"
    
    def __repr__(self):
        """Developer-friendly representation."""
        return f"Book(title='{self.title}', pages={self.pages})"
    
    def __len__(self):
        """Length of the book (number of pages)."""
        return self.pages
    
    def __eq__(self, other):
        """Check equality (same title and pages)."""
        return self.title == other.title and self.pages == other.pages
    
    def __lt__(self, other):
        """Less than comparison (by pages)."""
        return self.pages < other.pages

# Test special methods
book1 = Book("Clean Code", 464)
book2 = Book("Design Patterns", 395)
book3 = Book("Clean Code", 464)

# __str__ and __repr__
print(str(book1))  # Clean Code (464 pages)
print(repr(book1))  # Book(title='Clean Code', pages=464)

# __len__
print(f"Length: {len(book1)} pages")  # Length: 464 pages

# __eq__
print(f"book1 == book3: {book1 == book3}")  # True
print(f"book1 == book2: {book1 == book2}")  # False

# __lt__
print(f"book2 < book1: {book2 < book1}")  # True (395 < 464)

# Sorting works because of __lt__
books = [book1, book2]
books.sort()
print(f"Sorted: {[str(b) for b in books]}")
# Output: ['Design Patterns (395 pages)', 'Clean Code (464 pages)']

Real-World Example: Complete ML System

Let's build a complete machine learning system using OOP:

class DataLoader:
    """Load and prepare data."""
    
    def __init__(self, filepath):
        self.filepath = filepath
        self.data = None
    
    def load(self):
        """Load data from file (simulated)."""
        print(f"๐Ÿ“ Loading data from {self.filepath}...")
        # Simulate loading
        self.data = {
            "features": [[1, 2], [3, 4], [5, 6], [7, 8]],
            "labels": [0, 1, 0, 1]
        }
        print(f"โœ… Loaded {len(self.data['features'])} samples")
        return self.data

class DataPreprocessor:
    """Preprocess data for ML."""
    
    def __init__(self, normalize=True):
        self.normalize = normalize
    
    def preprocess(self, data):
        """Clean and prepare data."""
        print("๐Ÿ”ง Preprocessing data...")
        
        features = data["features"]
        labels = data["labels"]
        
        if self.normalize:
            print("  - Normalizing features...")
            # Simple min-max normalization
            features = [[x / 10 for x in row] for row in features]
        
        print("โœ… Preprocessing complete")
        return {"features": features, "labels": labels}

class Model:
    """Base model class."""
    
    def __init__(self, name):
        self.name = name
        self.trained = False
        self.accuracy = 0.0
    
    def train(self, data):
        """Train the model."""
        print(f"๐Ÿค– Training {self.name}...")
        features = data["features"]
        labels = data["labels"]
        print(f"  - Training samples: {len(features)}")
        # Simulate training
        self.trained = True
        self.accuracy = 0.87
        print(f"โœ… {self.name} trained! Accuracy: {self.accuracy:.1%}")
    
    def predict(self, features):
        """Make predictions."""
        if not self.trained:
            raise ValueError("Model must be trained first!")
        print(f"๐Ÿ”ฎ Making predictions with {self.name}...")
        return [0, 1, 0, 1]  # Dummy predictions

class MLPipeline:
    """Complete ML pipeline."""
    
    def __init__(self, data_path, model_name="DefaultModel"):
        self.data_loader = DataLoader(data_path)
        self.preprocessor = DataPreprocessor(normalize=True)
        self.model = Model(model_name)
        self.results = {}
    
    def run(self):
        """Execute the complete pipeline."""
        print("="*50)
        print("๐Ÿš€ Starting ML Pipeline")
        print("="*50)
        
        # Load data
        raw_data = self.data_loader.load()
        
        # Preprocess
        processed_data = self.preprocessor.preprocess(raw_data)
        
        # Train
        self.model.train(processed_data)
        
        # Test predictions
        test_features = [[9, 10]]
        predictions = self.model.predict(test_features)
        
        # Store results
        self.results = {
            "model_name": self.model.name,
            "accuracy": self.model.accuracy,
            "predictions": predictions
        }
        
        print("="*50)
        print("โœ… Pipeline Complete!")
        print(f"Model: {self.results['model_name']}")
        print(f"Accuracy: {self.results['accuracy']:.1%}")
        print("="*50)
        
        return self.results

# Run the complete pipeline
pipeline = MLPipeline("data.csv", model_name="RandomForest")
results = pipeline.run()

Best Practices for OOP

1. Use Descriptive Class Names

# Bad
class D:
    pass

# Good
class DataPreprocessor:
    pass

2. Keep Classes Focused (Single Responsibility)

# Bad (does too much)
class MLSystem:
    def load_data(self):
        pass
    
    def preprocess(self):
        pass
    
    def train(self):
        pass
    
    def deploy(self):
        pass

# Good (focused classes)
class DataLoader:
    def load_data(self):
        pass

class DataPreprocessor:
    def preprocess(self):
        pass

class ModelTrainer:
    def train(self):
        pass

3. Document Your Classes

class LinearRegression:
    """
    Linear regression model using gradient descent.
    
    Attributes:
        learning_rate (float): Step size for gradient descent
        epochs (int): Number of training iterations
        weights (list): Model weights after training
    
    Example:
        >>> model = LinearRegression(learning_rate=0.01)
        >>> model.train(X_train, y_train)
        >>> predictions = model.predict(X_test)
    """
    pass

4. Use Composition Over Inheritance

Sometimes has-a is better than is-a:

# Instead of: MLSystem inherits from DataLoader, Trainer, Evaluator (complex!)

# Better: MLSystem HAS these components
class MLSystem:
    def __init__(self):
        self.data_loader = DataLoader()  # Has-a DataLoader
        self.trainer = Trainer()          # Has-a Trainer
        self.evaluator = Evaluator()      # Has-a Evaluator

Common Beginner Mistakes

Mistake 1: Forgetting self

# Wrong
class Person:
    def __init__(name, age):  # Missing self!
        name = name  # Wrong!
        age = age    # Wrong!

# Right
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Mistake 2: Not Calling super().__init__()

# Wrong
class Dog(Animal):
    def __init__(self, name, breed):
        # Forgot to call parent constructor!
        self.breed = breed

# Right
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Dog")  # Call parent first!
        self.breed = breed

Mistake 3: Modifying Class Attributes Directly

# Wrong
class Counter:
    count = 0  # Class attribute
    
    def increment(self):
        Counter.count += 1  # Modifies for ALL instances

# Right
class Counter:
    def __init__(self):
        self.count = 0  # Instance attribute
    
    def increment(self):
        self.count += 1  # Only affects this instance

Conclusion: Your OOP Journey

Congratulations! You've learned the fundamentals of Python OOP:

โœ… Classes and Objects - Creating blueprints and instances
โœ… init Method - Initializing objects with data
โœ… Methods - Adding behavior to classes
โœ… Inheritance - Reusing code through parent-child relationships
โœ… Encapsulation - Protecting data with public/private attributes
โœ… Polymorphism - Same interface, different implementations
โœ… Special Methods - Customizing object behavior

Next Steps

  1. Practice daily - Create one class every day
  2. Refactor old code - Turn functions into classes
  3. Read library source code - See how professionals use OOP
  4. Build projects - Create ML systems using OOP principles
  5. Learn design patterns - Explore advanced OOP patterns

Quick Reference Cheat Sheet

# Basic class
class MyClass:
    """Class docstring."""
    
    def __init__(self, param):
        """Constructor."""
        self.param = param
    
    def method(self):
        """Instance method."""
        return self.param

# Inheritance
class Child(Parent):
    def __init__(self, param):
        super().__init__(param)  # Call parent

# Properties
class MyClass:
    @property
    def value(self):
        return self._value
    
    @value.setter
    def value(self, val):
        self._value = val

# Special methods
class MyClass:
    def __str__(self):
        return "string representation"
    
    def __len__(self):
        return 10

OOP is a journey, not a destination. Start with simple classes, build real projects, and gradually incorporate advanced concepts. You've got this! ๐Ÿš€


If you found this guide helpful and are building amazing Python projects with OOP, I'd love to hear about them! Share your progress with me on Twitter or connect on LinkedIn. Let's build great software together!

Support My Work

If this comprehensive guide helped you master Python OOP, understand classes and inheritance, or build better machine learning systems, I'd really appreciate your support! Creating detailed, beginner-friendly content like this takes significant time and effort. Your support helps me continue sharing knowledge and creating more helpful resources for aspiring developers and data scientists.

โ˜• Buy me a coffee - Every contribution, big or small, means the world to me and keeps me motivated to create more content!


Cover image by Juanjo Jaramillo on Unsplash

Related Blogs

Ojaswi Athghara

SDE, 4+ Years

ยฉ ojaswiat.com 2025-2027