Classes & Objects

0/5 in this phase0/54 across the roadmap

📖 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 (like this in 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 self as first parameter
  • Class methods — take cls as 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.__attr becomes obj._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

codeTap to expand ⛶
1# ============================================================
2# Basic class definition
3# ============================================================
4class BankAccount:
5 """A simple bank account with deposit and withdrawal."""
6
7 # Class attribute — shared by all instances
8 bank_name = "Python National Bank"
9 _total_accounts = 0
10
11 def __init__(self, owner: str, balance: float = 0.0):
12 """Initialize a new account."""
13 self.owner = owner # Public instance attribute
14 self._balance = balance # "Private" by convention
15 self.__id = id(self) # Name-mangled (harder to access)
16 BankAccount._total_accounts += 1
17
18 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 += amount
23 return self._balance
24
25 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 -= amount
32 return self._balance
33
34 def get_balance(self) -> float:
35 """Return current balance."""
36 return self._balance
37
38 def __str__(self) -> str:
39 """Human-readable string representation."""
40 return f"Account({self.owner}: ${self._balance:,.2f})"
41
42 def __repr__(self) -> str:
43 """Developer-friendly representation."""
44 return f"BankAccount(owner={self.owner!r}, balance={self._balance})"
45
46 @classmethod
47 def get_total_accounts(cls) -> int:
48 """Return total number of accounts created."""
49 return cls._total_accounts
50
51 @classmethod
52 def from_dict(cls, data: dict) -> "BankAccount":
53 """Alternative constructor from dictionary."""
54 return cls(owner=data["owner"], balance=data.get("balance", 0))
55
56 @staticmethod
57 def validate_amount(amount: float) -> bool:
58 """Validate that an amount is a positive number."""
59 return isinstance(amount, (int, float)) and amount > 0
60
61
62# ============================================================
63# Using the class
64# ============================================================
65# Create instances
66alice = BankAccount("Alice", 1000)
67bob = BankAccount("Bob")
68
69# Instance methods
70alice.deposit(500)
71print(alice) # Account(Alice: $1,500.00)
72print(repr(alice)) # BankAccount(owner='Alice', balance=1500)
73
74# Class method
75print(BankAccount.get_total_accounts()) # 2
76
77# Alternative constructor
78data = {"owner": "Charlie", "balance": 750}
79charlie = BankAccount.from_dict(data)
80
81# Static method
82print(BankAccount.validate_amount(100)) # True
83print(BankAccount.validate_amount(-50)) # False
84
85# ============================================================
86# Class vs Instance attributes
87# ============================================================
88class Dog:
89 species = "Canis familiaris" # Class attribute
90
91 def __init__(self, name, breed):
92 self.name = name # Instance attribute
93 self.breed = breed # Instance attribute
94
95fido = Dog("Fido", "Labrador")
96buddy = Dog("Buddy", "Poodle")
97
98# Both share the class attribute
99print(fido.species) # "Canis familiaris"
100print(buddy.species) # "Canis familiaris"
101
102# Modifying class attribute affects all instances
103Dog.species = "Canis lupus familiaris"
104print(fido.species) # "Canis lupus familiaris"
105
106# But assigning to instance creates a new instance attribute!
107fido.species = "Custom" # Creates instance attribute
108print(fido.species) # "Custom" (instance attribute)
109print(buddy.species) # "Canis lupus familiaris" (still class attribute)
110print(Dog.species) # "Canis lupus familiaris" (class attribute unchanged)
111
112# ============================================================
113# Name mangling (__double_underscore)
114# ============================================================
115class Secret:
116 def __init__(self):
117 self.__hidden = "secret value"
118
119s = Secret()
120# print(s.__hidden) # AttributeError!
121print(s._Secret__hidden) # "secret value" (mangled name)
122
123# Name mangling prevents accidental override in subclasses
124class Parent:
125 def __init__(self):
126 self.__value = "parent"
127
128class Child(Parent):
129 def __init__(self):
130 super().__init__()
131 self.__value = "child" # This is _Child__value, NOT _Parent__value
132
133c = Child()
134print(c._Parent__value) # "parent"
135print(c._Child__value) # "child"
136
137# ============================================================
138# @classmethod vs @staticmethod
139# ============================================================
140class Date:
141 def __init__(self, year, month, day):
142 self.year = year
143 self.month = month
144 self.day = day
145
146 @classmethod
147 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!
151
152 @classmethod
153 def today(cls):
154 """Create a Date for today."""
155 import datetime
156 t = datetime.date.today()
157 return cls(t.year, t.month, t.day)
158
159 @staticmethod
160 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 <= 31
163
164 def __str__(self):
165 return f"{self.year:04d}-{self.month:02d}-{self.day:02d}"
166
167d = Date.from_string("2024-06-15")
168print(d) # 2024-06-15
169print(Date.is_valid(2024, 13, 1)) # False

🏋️ Practice Exercise

Exercises:

  1. Create a Rectangle class with width and height. Add methods for area, perimeter, and is_square(). Include __str__, __repr__, and an @classmethod factory method from_square(side).

  2. Build a LinkedList class with Node inner class. Implement append, prepend, delete, find, and __str__ (to print the list).

  3. Create a Student class that tracks all created students (class attribute). Add class methods to get average grade, find top students, and count students.

  4. 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.

  5. Build a Config class that reads configuration from a dictionary, with @classmethod factories for loading from JSON file, environment variables, and defaults.

  6. Explain name mangling with a Parent/Child class example where both define self.__data.

⚠️ Common Mistakes

  • Forgetting self as the first parameter of instance methods. def method(self): — without self, 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 @staticmethod when @classmethod is more appropriate. If the method needs to create instances (factory method) or access class attributes, use @classmethod so subclasses work correctly.

  • Thinking double underscore __attr makes 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