Classes & Objects
📖 Concept
Everything in Python is an object — integers, strings, functions, even classes themselves. Python's OOP model is flexible and pragmatic, emphasizing "we're all consenting adults" rather than strict access control.
Class basics:
class MyClass:
class_attribute = "shared" # Shared by all instances
def __init__(self, value): # Constructor (initializer)
self.instance_attr = value # Unique to each instance
def method(self): # Instance method
return self.instance_attr
Key concepts:
self— explicit reference to the current instance (likethisin JS/Java, but always explicit in Python)__init__— initializer method (NOT a constructor;__new__is the actual constructor)- Instance attributes — belong to a specific object, set via
self.attr = value - Class attributes — shared by all instances, defined in the class body
- Instance methods — take
selfas first parameter - Class methods — take
clsas first parameter, decorated with@classmethod - Static methods — no implicit first parameter, decorated with
@staticmethod
Naming conventions for access control:
public— accessible everywhere_private— internal use (convention, not enforced)__mangled— name mangling:obj.__attrbecomesobj._ClassName__attr
Python doesn't have true private attributes — the single underscore _ is a convention that says "please don't use this from outside," and the double underscore __ triggers name mangling to prevent accidental access in subclasses.
💻 Code Example
1# ============================================================2# Basic class definition3# ============================================================4class BankAccount:5 """A simple bank account with deposit and withdrawal."""67 # Class attribute — shared by all instances8 bank_name = "Python National Bank"9 _total_accounts = 01011 def __init__(self, owner: str, balance: float = 0.0):12 """Initialize a new account."""13 self.owner = owner # Public instance attribute14 self._balance = balance # "Private" by convention15 self.__id = id(self) # Name-mangled (harder to access)16 BankAccount._total_accounts += 11718 def deposit(self, amount: float) -> float:19 """Deposit money into the account."""20 if amount <= 0:21 raise ValueError("Deposit amount must be positive")22 self._balance += amount23 return self._balance2425 def withdraw(self, amount: float) -> float:26 """Withdraw money from the account."""27 if amount <= 0:28 raise ValueError("Withdrawal amount must be positive")29 if amount > self._balance:30 raise ValueError("Insufficient funds")31 self._balance -= amount32 return self._balance3334 def get_balance(self) -> float:35 """Return current balance."""36 return self._balance3738 def __str__(self) -> str:39 """Human-readable string representation."""40 return f"Account({self.owner}: ${self._balance:,.2f})"4142 def __repr__(self) -> str:43 """Developer-friendly representation."""44 return f"BankAccount(owner={self.owner!r}, balance={self._balance})"4546 @classmethod47 def get_total_accounts(cls) -> int:48 """Return total number of accounts created."""49 return cls._total_accounts5051 @classmethod52 def from_dict(cls, data: dict) -> "BankAccount":53 """Alternative constructor from dictionary."""54 return cls(owner=data["owner"], balance=data.get("balance", 0))5556 @staticmethod57 def validate_amount(amount: float) -> bool:58 """Validate that an amount is a positive number."""59 return isinstance(amount, (int, float)) and amount > 0606162# ============================================================63# Using the class64# ============================================================65# Create instances66alice = BankAccount("Alice", 1000)67bob = BankAccount("Bob")6869# Instance methods70alice.deposit(500)71print(alice) # Account(Alice: $1,500.00)72print(repr(alice)) # BankAccount(owner='Alice', balance=1500)7374# Class method75print(BankAccount.get_total_accounts()) # 27677# Alternative constructor78data = {"owner": "Charlie", "balance": 750}79charlie = BankAccount.from_dict(data)8081# Static method82print(BankAccount.validate_amount(100)) # True83print(BankAccount.validate_amount(-50)) # False8485# ============================================================86# Class vs Instance attributes87# ============================================================88class Dog:89 species = "Canis familiaris" # Class attribute9091 def __init__(self, name, breed):92 self.name = name # Instance attribute93 self.breed = breed # Instance attribute9495fido = Dog("Fido", "Labrador")96buddy = Dog("Buddy", "Poodle")9798# Both share the class attribute99print(fido.species) # "Canis familiaris"100print(buddy.species) # "Canis familiaris"101102# Modifying class attribute affects all instances103Dog.species = "Canis lupus familiaris"104print(fido.species) # "Canis lupus familiaris"105106# But assigning to instance creates a new instance attribute!107fido.species = "Custom" # Creates instance attribute108print(fido.species) # "Custom" (instance attribute)109print(buddy.species) # "Canis lupus familiaris" (still class attribute)110print(Dog.species) # "Canis lupus familiaris" (class attribute unchanged)111112# ============================================================113# Name mangling (__double_underscore)114# ============================================================115class Secret:116 def __init__(self):117 self.__hidden = "secret value"118119s = Secret()120# print(s.__hidden) # AttributeError!121print(s._Secret__hidden) # "secret value" (mangled name)122123# Name mangling prevents accidental override in subclasses124class Parent:125 def __init__(self):126 self.__value = "parent"127128class Child(Parent):129 def __init__(self):130 super().__init__()131 self.__value = "child" # This is _Child__value, NOT _Parent__value132133c = Child()134print(c._Parent__value) # "parent"135print(c._Child__value) # "child"136137# ============================================================138# @classmethod vs @staticmethod139# ============================================================140class Date:141 def __init__(self, year, month, day):142 self.year = year143 self.month = month144 self.day = day145146 @classmethod147 def from_string(cls, date_string):148 """Parse 'YYYY-MM-DD' string. Uses cls so subclasses work."""149 year, month, day = map(int, date_string.split("-"))150 return cls(year, month, day) # cls, not Date!151152 @classmethod153 def today(cls):154 """Create a Date for today."""155 import datetime156 t = datetime.date.today()157 return cls(t.year, t.month, t.day)158159 @staticmethod160 def is_valid(year, month, day):161 """Check if date is valid. No class/instance access needed."""162 return 1 <= month <= 12 and 1 <= day <= 31163164 def __str__(self):165 return f"{self.year:04d}-{self.month:02d}-{self.day:02d}"166167d = Date.from_string("2024-06-15")168print(d) # 2024-06-15169print(Date.is_valid(2024, 13, 1)) # False
🏋️ Practice Exercise
Exercises:
Create a
Rectangleclass with width and height. Add methods for area, perimeter, andis_square(). Include__str__,__repr__, and an@classmethodfactory methodfrom_square(side).Build a
LinkedListclass withNodeinner class. Implementappend,prepend,delete,find, and__str__(to print the list).Create a
Studentclass that tracks all created students (class attribute). Add class methods to get average grade, find top students, and count students.Demonstrate the difference between class attributes and instance attributes: create a class where modifying a class-level mutable attribute (list) from one instance affects all instances.
Build a
Configclass that reads configuration from a dictionary, with@classmethodfactories for loading from JSON file, environment variables, and defaults.Explain name mangling with a Parent/Child class example where both define
self.__data.
⚠️ Common Mistakes
Forgetting
selfas the first parameter of instance methods.def method(self):— withoutself, you'll get 'takes 0 positional arguments but 1 was given'.Using a mutable class attribute (like a list) that gets shared across instances:
class Foo: items = []— all instances share the SAME list. Define mutable attributes in__init__instead.Confusing
__init__with a constructor.__init__is the initializer — the object already exists when__init__runs.__new__is the actual constructor that creates the object.Using
@staticmethodwhen@classmethodis more appropriate. If the method needs to create instances (factory method) or access class attributes, use@classmethodso subclasses work correctly.Thinking double underscore
__attrmakes it truly private. It only triggers name mangling (_ClassName__attr). Python has no true private attributes — everything is accessible.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Classes & Objects