In Python, a class is a blueprint for creating objects. Think of it like an architectural blueprint for a house: the blueprint itself is not a house, but it defines exactly how every house built from it will be structured — where the doors go, how many rooms there are, and what materials to use. From that single blueprint, you can build dozens of houses (objects), each with its own address and paint color, but all sharing the same fundamental design.
This concept is the foundation of Object-Oriented Programming (OOP). Instead of writing loose functions that pass data around (procedural style), OOP lets you bundle related data and behavior together into classes. The result is code that is easier to reason about, test, and maintain — especially as your codebase grows.
An object (also called an instance) is a concrete realization of a class, living in memory with its own data. The process of creating an object from a class is called instantiation.
At its simplest, a Python class is defined with the class keyword followed by the class name in PascalCase and a colon.
class ClassName:
"""Docstring describing the class."""
# class body: variables, methods, etc.
pass
# Creating an instance
obj = ClassName()
Note: the parentheses after the class name are optional when there is no explicit parent class. You will see both class Foo: and class Foo(): in the wild — they are equivalent when no inheritance is involved.
__init__() ConstructorThe __init__() method is Python’s constructor. It is called automatically every time you create a new instance. Its job is to initialize the object’s state — setting up the attributes that each instance needs to function.
class User:
def __init__(self, name, age):
self.name = name # instance variable
self.age = age # instance variable
# Instantiation triggers __init__
user1 = User("Alice", 30)
print(user1.name) # Alice
print(user1.age) # 30
When you call User("Alice", 30), Python internally creates a new User object and then calls User.__init__(new_object, "Alice", 30). You never call __init__ directly.
self ParameterEvery instance method in Python receives self as its first argument. self is a reference to the current instance of the class — it is how the method knows which object’s data to read or modify.
class Dog:
def __init__(self, name):
self.name = name # 'self.name' belongs to THIS specific dog
def bark(self):
# 'self' lets us access instance data inside methods
return f"{self.name} says Woof!"
rex = Dog("Rex")
buddy = Dog("Buddy")
print(rex.bark()) # Rex says Woof!
print(buddy.bark()) # Buddy says Woof!
self is not a reserved keyword — you could technically name it anything — but using self is a universal Python convention. Breaking this convention will confuse every developer who reads your code.
This distinction trips up many developers, so it is worth understanding clearly.
__init__ (or other methods) using self.variable_name. Each object gets its own independent copy.class Employee:
# Class variable — shared by ALL instances
company = "TechCorp"
employee_count = 0
def __init__(self, name, role):
# Instance variables — unique to EACH instance
self.name = name
self.role = role
Employee.employee_count += 1
emp1 = Employee("Alice", "Engineer")
emp2 = Employee("Bob", "Designer")
# Class variable is the same for both
print(emp1.company) # TechCorp
print(emp2.company) # TechCorp
# Instance variables are independent
print(emp1.name) # Alice
print(emp2.name) # Bob
# Class variable tracks state across all instances
print(Employee.employee_count) # 2
Instance methods are the most common type of method. They take self as the first parameter and can freely access and modify the object’s state.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
return self.balance
def get_balance(self):
return self.balance
def __str__(self):
return f"BankAccount(owner='{self.owner}', balance={self.balance})"
# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
print(account) # BankAccount(owner='Alice', balance=1300)
@classmethod)A class method receives the class itself (conventionally named cls) as its first argument instead of an instance. It cannot access instance-specific data, but it can read and modify class-level state. Class methods are commonly used as alternative constructors — factory methods that create instances in different ways.
class Employee:
employee_count = 0
def __init__(self, name, role, salary):
self.name = name
self.role = role
self.salary = salary
Employee.employee_count += 1
@classmethod
def from_string(cls, employee_string):
"""Alternative constructor: create Employee from a dash-separated string."""
name, role, salary = employee_string.split("-")
return cls(name, role, float(salary))
@classmethod
def get_employee_count(cls):
return cls.employee_count
# Standard construction
emp1 = Employee("Alice", "Engineer", 95000)
# Alternative construction via class method
emp2 = Employee.from_string("Bob-Designer-85000")
print(emp2.name) # Bob
print(emp2.role) # Designer
print(emp2.salary) # 85000.0
print(Employee.get_employee_count()) # 2
Notice that from_string calls cls(...) instead of Employee(...). This makes the class method work correctly with subclasses — the cls parameter will refer to the subclass when called from one.
@staticmethod)A static method receives neither self nor cls. It behaves like a regular function but lives inside the class’s namespace for organizational purposes. Use it for utility logic that is related to the class but does not need to access any instance or class state.
class MathUtils:
@staticmethod
def is_even(number):
return number % 2 == 0
@staticmethod
def factorial(n):
if n < 0:
raise ValueError("Factorial not defined for negative numbers")
result = 1
for i in range(2, n + 1):
result *= i
return result
print(MathUtils.is_even(4)) # True
print(MathUtils.factorial(5)) # 120
| Method Type | First Parameter | Can Access Instance State? | Can Access Class State? | Use Case |
|---|---|---|---|---|
| Instance method | self |
Yes | Yes | Most methods — operating on object data |
| Class method | cls |
No | Yes | Alternative constructors, class-level operations |
| Static method | None | No | No | Utility functions related to the class |
@property Decorator)The @property decorator lets you define methods that behave like attributes. This is the Pythonic way to implement computed attributes and controlled access — no need for Java-style get_x() / set_x() methods.
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Get the temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero is not possible")
self._celsius = value
@property
def fahrenheit(self):
"""Get the temperature in Fahrenheit (computed from Celsius)."""
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5 / 9
# Usage
temp = Temperature(100)
print(temp.celsius) # 100
print(temp.fahrenheit) # 212.0
temp.fahrenheit = 32
print(temp.celsius) # 0.0
# Validation kicks in automatically
# temp.celsius = -300 # Raises ValueError
The beauty of @property is that callers use simple attribute syntax (temp.fahrenheit) while the class retains full control over how values are computed, validated, and stored.
Python uses dunder (double underscore) methods to let your classes integrate with built-in language features like print(), len(), comparison operators, and more. Here are the most important ones.
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, name, price):
self.items.append({"name": name, "price": price})
def __str__(self):
"""Human-readable string — used by print() and str()."""
item_list = ", ".join(item["name"] for item in self.items)
return f"Cart({item_list}) - Total: ${self.__get_total():.2f}"
def __repr__(self):
"""Developer-readable string — used in debugger, REPL, and logs."""
return f"ShoppingCart(items={self.items!r})"
def __len__(self):
"""Called by len() — returns the number of items."""
return len(self.items)
def __eq__(self, other):
"""Called by == operator."""
if not isinstance(other, ShoppingCart):
return NotImplemented
return self.items == other.items
def __get_total(self):
return sum(item["price"] for item in self.items)
# Usage
cart = ShoppingCart()
cart.add_item("Python Book", 39.99)
cart.add_item("Mechanical Keyboard", 129.00)
print(str(cart)) # Cart(Python Book, Mechanical Keyboard) - Total: $168.99
print(repr(cart)) # ShoppingCart(items=[{'name': 'Python Book', 'price': 39.99}, ...])
print(len(cart)) # 2
A rule of thumb: always define __repr__ for every class you write. When you are debugging at 2 AM, you will thank yourself for having a clear representation of your objects instead of <MyClass object at 0x7f...>.
Python does not have true private or protected access like Java or C++. Instead, it relies on naming conventions that signal developer intent.
| Convention | Example | Meaning |
|---|---|---|
| No prefix | self.name |
Public — use freely from outside the class |
Single underscore _ |
self._name |
Protected by convention — "internal use, don't touch unless you know what you're doing" |
Double underscore __ |
self.__name |
Name mangling — Python renames it to _ClassName__name to prevent accidental access from subclasses |
class Account:
def __init__(self, owner, balance):
self.owner = owner # public
self._account_type = "checking" # protected by convention
self.__balance = balance # name-mangled
def get_balance(self):
return self.__balance
acct = Account("Alice", 5000)
print(acct.owner) # Alice — works fine
print(acct._account_type) # checking — works, but signals "internal"
# print(acct.__balance) # AttributeError!
print(acct._Account__balance) # 5000 — name mangling, avoid this
print(acct.get_balance()) # 5000 — the intended way
In practice, most Python code uses the single underscore convention. Double underscore name mangling is rarely needed unless you are designing a class hierarchy and want to avoid name collisions with subclasses.
Let us put several concepts together into a well-structured class.
class BankAccount:
"""A bank account supporting deposits, withdrawals, and transaction history."""
# Class variable — shared interest rate for all accounts
interest_rate = 0.02
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
self._transactions = []
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
self._transactions.append(f"+{amount}")
return self
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self._balance:
raise ValueError(f"Insufficient funds: balance is {self._balance}, requested {amount}")
self._balance -= amount
self._transactions.append(f"-{amount}")
return self
def apply_interest(self):
interest = self._balance * self.interest_rate
self._balance += interest
self._transactions.append(f"+{interest:.2f} (interest)")
return self
@classmethod
def from_dict(cls, data):
"""Alternative constructor: create account from a dictionary."""
return cls(owner=data["owner"], balance=data.get("balance", 0))
@staticmethod
def validate_amount(amount):
"""Utility: check if an amount is valid for transactions."""
return isinstance(amount, (int, float)) and amount > 0
def __str__(self):
return f"BankAccount(owner='{self.owner}', balance=${self._balance:,.2f})"
def __repr__(self):
return f"BankAccount(owner={self.owner!r}, balance={self._balance!r})"
# Usage
account = BankAccount("Alice", 1000)
account.deposit(500).withdraw(200).apply_interest()
print(account) # BankAccount(owner='Alice', balance=$1,326.00)
print(account.balance) # 1326.0
# Alternative constructor
data = {"owner": "Bob", "balance": 5000}
bob_account = BankAccount.from_dict(data)
print(bob_account) # BankAccount(owner='Bob', balance=$5,000.00)
# Static utility
print(BankAccount.validate_amount(100)) # True
print(BankAccount.validate_amount(-50)) # False
1. Forgetting the self parameter
This is the single most common mistake for developers coming from other languages.
# WRONG — will raise TypeError when called
class User:
def greet():
return "Hello!"
user = User()
# user.greet() # TypeError: greet() takes 0 positional arguments but 1 was given
# CORRECT
class User:
def greet(self):
return "Hello!"
2. Mutable class variables shared between instances
This is a subtle and dangerous bug. If a class variable is a mutable object (like a list or dictionary), all instances share the same object in memory.
# WRONG — all instances share the SAME list
class Student:
grades = [] # class variable (mutable!)
def add_grade(self, grade):
self.grades.append(grade)
s1 = Student()
s2 = Student()
s1.add_grade(95)
print(s2.grades) # [95] — s2 sees s1's grade!
# CORRECT — each instance gets its own list
class Student:
def __init__(self):
self.grades = [] # instance variable
def add_grade(self, grade):
self.grades.append(grade)
s1 = Student()
s2 = Student()
s1.add_grade(95)
print(s2.grades) # [] — isolated as expected
3. Confusing class variables and instance variables
When you read an attribute, Python first checks the instance, then falls back to the class. When you assign through an instance, it creates a new instance variable that shadows the class variable.
class Config:
debug = False # class variable
c1 = Config()
c2 = Config()
c1.debug = True # creates an INSTANCE variable on c1, does NOT modify the class variable
print(c1.debug) # True (instance variable)
print(c2.debug) # False (still reading class variable)
print(Config.debug) # False (class variable unchanged)
1. Use descriptive class names in PascalCase. Follow PEP 8: BankAccount, not bank_account or bankAccount.
2. Keep classes focused (Single Responsibility Principle). A class should do one thing well. If your class has methods for sending emails, querying databases, and rendering HTML, it is doing too much — split it up.
3. Use @property instead of getter/setter methods. Python is not Java. Replace get_balance() / set_balance() with a @property. Your callers get clean attribute-style access while you retain full control.
4. Always define __repr__ for debugging. A good __repr__ should ideally return a string that could recreate the object: BankAccount(owner='Alice', balance=1000).
5. Prefer composition over deep inheritance. Before creating a class hierarchy five levels deep, ask yourself if one class could simply hold a reference to another.
6. Initialize all instance attributes in __init__. Do not create new attributes in random methods — it makes the class harder to understand and can lead to AttributeError surprises.
__init__ is the constructor — it runs automatically when you create an instance.self refers to the current instance and is required as the first parameter of every instance method.self.x) belong to one object; class variables belong to the class and are shared.@property decorator replaces getters and setters with clean, Pythonic attribute access.__str__, __repr__, __len__, __eq__) let your classes integrate seamlessly with Python's built-in operations._ (protected), __ (name-mangled).