A Class is a blueprint / template for creating objects. It defines what data (attributes) an object will hold and what actions (methods) it can perform. A class does not occupy memory by itself — memory is allocated only when an object (instance) is created from it.
Explanation
Real-World Analogy
Think of a class as an architectural blueprint of a house.
The blueprint itself is not a house — it describes how a house should look (rooms, walls, windows).
Every actual house built from that blueprint is an object (an instance of the blueprint).
Multiple different houses can be built from the same blueprint, each with their own address, color, and furniture — but they all share the same structure.
Blueprint Concept
OOP Equivalent
Blueprint / Template
Class
Actual House built
Object / Instance
Blueprint’s room plan
Attributes (fields)
Blueprint’s instructions
Methods (functions)
Building the house
Instantiation (new keyword)
Why Do We Need Classes?
Without classes, we’d write duplicate code for every object (e.g., writing a separate function for every user, every car, etc.). Classes solve this through reusability, organization, and abstraction.
WITHOUT classes: WITH a class:
user1_name = "Alice" class User:
user1_age = 30 def __init__(self, name, age):
user2_name = "Bob" self.name = name
user2_age = 25 self.age = age
...
user1 = User("Alice", 30)
user2 = User("Bob", 25)
Classes group related data and behavior together, making code easier to read, maintain, and scale.
Anatomy of a Class
A class has multiple distinct parts. Each part serves a specific role:
Access modifiers control who can see and use an attribute or method from outside the class. They are a key part of Encapsulation.
Modifier
Syntax (Python)
Syntax (Java/C#/C++)
Who Can Access
Public
self.name (no prefix)
public
Accessible from anywhere
Protected
self._name (single underscore)
protected
Accessible within the class and subclasses
Private
self.__name (double underscore)
private
Accessible only within the class itself
flowchart LR
subgraph "Outside World"
EX[External Code]
end
subgraph "Subclass"
SUB[Child Class]
end
subgraph "Class"
PUB["✅ Public\nself.name"]
PRO["⚠️ Protected\nself._name"]
PRI["❌ Private\nself.__name"]
end
EX -- "can access" --> PUB
EX -- "should not access" --> PRO
EX -- "cannot access" --> PRI
SUB -- "can access" --> PUB
SUB -- "can access" --> PRO
SUB -- "cannot access" --> PRI
Python enforces private access through name mangling — self.__name is internally stored as self._ClassName__name. It's a convention, not a hard lock. Java/C++ enforce it strictly at compile time.
Class vs Object (Instance)
A class and an object are not the same thing. A class is a template; an object is a living realization of that template.
Concept
Class
Object
What it is
Blueprint / Template
Actual usable entity in memory
Memory
No memory allocated (just a definition)
Memory allocated at creation
How many
Defined once
Can have thousands of instances
Created with
class keyword
Constructor call (Dog("Buddy", 3))
Example
class Car:
my_car = Car("Tesla", "Model 3")
Memory Model: What Happens When You Create an Object
─── Class Definition (stored in code segment, no heap allocation) ───
class Car: ← Blueprint lives here
def __init__(self, brand, model):
self.brand = brand
self.model = model
def drive(self):
print(f"{self.brand} is driving!")
─── After: my_car = Car("Toyota", "Corolla") ───
STACK HEAP
┌──────────┐ ┌──────────────────────────────┐
│ my_car │──────────────▶│ Object: Car │
│ (ref ptr)│ │ ─────────────────────────── │
└──────────┘ │ brand = "Toyota" │
│ model = "Corolla" │
│ [methods → shared via class] │
└──────────────────────────────┘
─── After: your_car = Car("Honda", "Civic") ───
STACK HEAP
┌──────────┐ ┌──────────────────────────────┐
│ your_car │──────────────▶│ Object: Car │
│ (ref ptr)│ │ brand = "Honda" │
└──────────┘ │ model = "Civic" │
└──────────────────────────────┘
Note: Both objects SHARE the same class methods (drive),
but have their OWN brand/model attribute copies.
Static vs Instance Members
Feature
Instance Member
Static Member
Belongs to
Each individual object
The class itself
Access via
object.attribute
ClassName.attribute
Memory
Separate copy per object
Single shared copy
Can access
Instance + class data
Class data only (no self)
Use case
Per-object state (name, age)
Shared counters, constants, utilities
class Counter: count = 0 # Static/Class attribute def __init__(self, name): self.name = name # Instance attribute Counter.count += 1 # Modify shared class attribute @staticmethod def get_count(): return Counter.counta = Counter("Alpha")b = Counter("Beta")c = Counter("Gamma")print(Counter.get_count()) # 3 — shared count across all instancesprint(a.name) # Alpha — unique to 'a'
Class Relationships
Classes rarely exist in isolation. They relate to each other in structured ways:
classDiagram
direction LR
class Animal {
+String name
+speak() void
}
class Dog {
+String breed
+fetch() void
}
class Owner {
+String ownerName
}
class Collar {
+String material
}
Animal <|-- Dog : Inheritance (is-a)
Owner "1" o-- "0..*" Dog : Aggregation (has-a, independent)
Dog "1" *-- "1" Collar : Composition (has-a, dependent)
Relationship
Symbol
Meaning
Example
Inheritance
`<
—`
Child is-a Parent. Child inherits all parent behavior
Composition
*--
Strong ownership — child cannot exist without parent
Collar cannot exist without Dog
Aggregation
o--
Weak relationship — child can exist independently
Owner has Dog, but dog can survive without owner
Association
-->
General relationship — one class uses another
Car uses Engine
How It Works
Step-by-Step: Class Creation to Object Use
1. DEFINE THE CLASS
└─ Use 'class' keyword
└─ Name it (PascalCase: MyClass)
└─ Define attributes in __init__
└─ Define methods as functions inside
2. INSTANTIATE AN OBJECT
└─ Call ClassName(args)
└─ Python calls __new__() to allocate memory
└─ Python calls __init__() to set attribute values
└─ Returns the object reference
3. USE THE OBJECT
└─ Access attributes: obj.attribute
└─ Call methods: obj.method()
└─ Modify state: obj.attribute = new_value
4. OBJECT IS DESTROYED
└─ Goes out of scope / garbage collected
└─ __del__() called (destructor) if defined
└─ Memory freed
Lifecycle Flowchart
flowchart TD
A["📝 Class Definition\n(Blueprint created)"] --> B["🏗️ Instantiation\nobj = MyClass(args)"]
B --> C["🔧 __new__() called\nMemory allocated on heap"]
C --> D["⚙️ __init__() called\nAttributes initialized with values"]
D --> E["✅ Object ready to use\nobj.attribute / obj.method()"]
E --> F{Object still needed?}
F -- "Yes" --> E
F -- "No (out of scope)" --> G["🗑️ __del__() called\nCleanup / destructor runs"]
G --> H["♻️ Memory freed by\nGarbage Collector"]
Implementation
Full class implementation of a BankAccount — demonstrating attributes, constructor, instance methods, class methods, static methods, access modifiers, and __str__ / __repr__ dunder methods.
Languages: Python · Cpp · Java · Java Script · CSharp
# ─── Python ──────────────────────────────────────────────────────────class BankAccount: """A class representing a bank account.""" # Class attribute (shared by all accounts) bank_name: str = "Code Bank" _total_accounts: int = 0 def __init__(self, owner: str, initial_balance: float = 0.0): # Instance attributes (unique per object) self.owner: str = owner # public self.__balance: float = initial_balance # private (name-mangled) self._account_id: str = f"ACC-{BankAccount._total_accounts + 1:04d}" # protected BankAccount._total_accounts += 1 # ── Instance Methods ────────────────────────────── def deposit(self, amount: float) -> None: """Add money to the account.""" if amount <= 0: raise ValueError("Deposit amount must be positive.") self.__balance += amount print(f"✅ Deposited ${amount:.2f}. New balance: ${self.__balance:.2f}") def withdraw(self, amount: float) -> bool: """Remove money if sufficient funds exist.""" if amount <= 0: raise ValueError("Withdrawal must be positive.") if amount > self.__balance: print(f"❌ Insufficient funds. Balance: ${self.__balance:.2f}") return False self.__balance -= amount print(f"✅ Withdrew ${amount:.2f}. New balance: ${self.__balance:.2f}") return True def get_balance(self) -> float: """Getter for the private __balance attribute.""" return self.__balance # ── Class Method ────────────────────────────────── @classmethod def get_total_accounts(cls) -> int: """Returns how many accounts have been created.""" return cls._total_accounts # ── Static Method ───────────────────────────────── @staticmethod def is_valid_amount(amount: float) -> bool: """Utility: checks if an amount is a valid positive number.""" return isinstance(amount, (int, float)) and amount > 0 # ── Dunder Methods (Magic Methods) ──────────────── def __str__(self) -> str: """Human-readable string for print().""" return f"[{self._account_id}] Owner: {self.owner} | Balance: ${self.__balance:.2f}" def __repr__(self) -> str: """Developer-friendly representation.""" return f"BankAccount(owner='{self.owner}', balance={self.__balance})"# ── Usage ────────────────────────────────────────────────acc1 = BankAccount("Alice", 500.0)acc2 = BankAccount("Bob")acc1.deposit(200) # ✅ Deposited $200.00. New balance: $700.00acc1.withdraw(100) # ✅ Withdrew $100.00. New balance: $600.00acc1.withdraw(800) # ❌ Insufficient funds. Balance: $600.00print(acc1) # [ACC-0001] Owner: Alice | Balance: $600.00print(repr(acc1)) # BankAccount(owner='Alice', balance=600.0)print(BankAccount.get_total_accounts()) # 2print(BankAccount.is_valid_amount(-50)) # Falseprint(BankAccount.bank_name) # Code Bank
Object Instantiation: O(k) where k is the number of attributes initialized.
Attribute Access: O(1) — direct memory lookup via object reference.
Method Call: O(1) — calling overhead; actual complexity depends on the method body.
Space per object: O(k) — one value stored per attribute.
Operation
Time Complexity
Notes
Create object (__init__)
O(k)
k = number of attributes initialized
Access attribute (obj.attr)
O(1)
Direct pointer lookup via __dict__
Call method (obj.method())
O(1) + method body
Method resolution is O(1) in most languages
Attribute lookup (Python)
O(1) average
Hash table lookup in __dict__
Delete object (del obj)
O(1)
Reference removed; GC handles memory
When to Use a Class
flowchart TD
Q{"Do you have data\nAND related behavior?"}
Q -- "Yes" --> A{"Will you create\nmultiple instances?"}
Q -- "No (just functions)" --> R1["❌ Use standalone functions\nor a module"]
A -- "Yes" --> R2["✅ Use a Class\nGroup related state + behavior"]
A -- "No (single use)" --> B{"Do related concepts\nform a hierarchy?"}
B -- "Yes (parent/child)" --> R3["✅ Use Classes with Inheritance"]
B -- "No" --> R4["⚠️ Consider a simple dict/struct\nor dataclass"]
✅ Use a Class When
You need to represent a real-world entity (User, Car, BankAccount, Product).
You have multiple instances that share the same structure but different data.
You want to encapsulate related data and behavior together.
You plan to use inheritance to create specialized versions of an entity.
You need state to persist across multiple method calls.
❌ Avoid a Class When
You only need a single function with no related data.
Your data has no behavior — a simple dict, namedtuple, or @dataclass is enough.
You’re writing quick scripts or one-off utilities — plain functions are simpler.