Classes and objects are the foundation of object-oriented programming (OOP) in Python. They allow you to organize code by grouping related data (attributes) and behavior (methods) together, creating reusable blueprints for modeling real-world entities and abstract concepts.
A class is a blueprint or template that defines what data and methods objects will have. An object (also called an instance) is a specific realization of a class with actual values for its attributes.
# Class definition - the blueprint
class Dog:
def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age # Instance attribute
def bark(self): # Instance method
return f"{self.name} says woof!"
# Object creation - actual instances
dog1 = Dog("Buddy", 3) # Creates an object
dog2 = Dog("Max", 5) # Creates another object
# Each object has its own data
print(dog1.name) # "Buddy"
print(dog2.name) # "Max"
print(dog1.bark()) # "Buddy says woof!"class ClassName:
"""Optional class docstring explaining what this class represents."""
# Class attributes (shared by all instances)
species = "Canis lupus"
def __init__(self, parameter1, parameter2):
"""Constructor method - runs when object is created."""
self.attribute1 = parameter1 # Instance attribute
self.attribute2 = parameter2 # Instance attribute
def method_name(self, parameter):
"""Instance method - operates on individual objects."""
# Method implementation
return some_value# Class names use PascalCase (CapWords)
class BankAccount: # Good
class bankAccount: # Avoid - not Pythonic
class bank_account: # Avoid - use for functions/variables
# Method and attribute names use snake_case
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name # Good
self.lastName = last_name # Avoid - not Pythonic
def get_full_name(self): # Good
return f"{self.first_name} {self.last_name}"
def getFull_Name(self): # Avoid - not Pythonic
passThe __init__ method is Python's constructor - it's automatically called when you create a new object:
class Student:
def __init__(self, name, student_id, major="Undeclared"):
"""Initialize a new student object."""
self.name = name
self.student_id = student_id
self.major = major
self.grades = [] # Start with empty grade list
self.enrollment_date = datetime.now()
# You can perform validation in __init__
if not isinstance(student_id, str) or len(student_id) != 8:
raise ValueError("Student ID must be 8-character string")
def __str__(self):
"""String representation for human reading."""
return f"Student({self.name}, ID: {self.student_id})"
# Creating objects calls __init__ automatically
student1 = Student("Alice", "12345678", "Computer Science")
student2 = Student("Bob", "87654321") # Uses default major
print(student1) # Student(Alice, ID: 12345678)
print(student1.major) # Computer Science
print(student2.major) # Undeclaredclass Rectangle:
def __init__(self, width, height=None):
"""Create rectangle. If height not given, creates a square."""
self.width = width
self.height = height if height is not None else width
# Validation in constructor
if width <= 0 or self.height <= 0:
raise ValueError("Dimensions must be positive")
def get_area(self):
return self.width * self.height
# Different ways to create rectangles
rect1 = Rectangle(5, 3) # 5x3 rectangle
rect2 = Rectangle(4) # 4x4 square (height defaults to width)Instance attributes belong to individual objects and can have different values for each instance:
class Employee:
def __init__(self, name, salary):
self.name = name # Instance attribute
self.salary = salary # Instance attribute
self.performance_reviews = [] # Each employee has own list
emp1 = Employee("Alice", 75000)
emp2 = Employee("Bob", 80000)
print(emp1.name) # Alice
print(emp2.name) # Bob
print(emp1.salary) # 75000
print(emp2.salary) # 80000Class attributes are shared by all instances of the class:
class Employee:
company_name = "TechCorp" # Class attribute
total_employees = 0 # Class attribute
benefits = ["Health", "Dental"] # Class attribute
def __init__(self, name, salary):
self.name = name # Instance attribute
self.salary = salary # Instance attribute
Employee.total_employees += 1 # Modify class attribute
@classmethod
def get_company_info(cls):
return f"{cls.company_name} has {cls.total_employees} employees"
# All instances share class attributes
emp1 = Employee("Alice", 75000)
emp2 = Employee("Bob", 80000)
print(emp1.company_name) # TechCorp
print(emp2.company_name) # TechCorp
print(Employee.total_employees) # 2
print(Employee.get_company_info()) # TechCorp has 2 employees
# Modifying class attribute affects all instances
Employee.company_name = "NewTechCorp"
print(emp1.company_name) # NewTechCorp
print(emp2.company_name) # NewTechCorp# Dangerous - all instances share the same list
class StudentWrong:
grades = [] # This is shared by ALL students!
def __init__(self, name):
self.name = name
def add_grade(self, grade):
self.grades.append(grade) # Modifies shared list
student1 = StudentWrong("Alice")
student2 = StudentWrong("Bob")
student1.add_grade(85)
student2.add_grade(92)
print(student1.grades) # [85, 92] - Alice sees Bob's grade!
print(student2.grades) # [85, 92] - Bob sees Alice's grade!
# Correct - each instance gets its own list
class StudentCorrect:
def __init__(self, name):
self.name = name
self.grades = [] # Each student gets own list
def add_grade(self, grade):
self.grades.append(grade)Instance methods are functions that belong to objects and operate on their data through the self parameter:
class BankAccount:
def __init__(self, account_holder, initial_balance=0):
self.account_holder = account_holder
self.balance = initial_balance
self.transaction_history = []
def deposit(self, amount):
"""Add money to the account."""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount
self.transaction_history.append(f"Deposited ${amount}")
return self.balance
def withdraw(self, amount):
"""Remove money from the account."""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
self.transaction_history.append(f"Withdrew ${amount}")
return self.balance
def get_balance(self):
"""Return current balance."""
return self.balance
def get_transaction_history(self):
"""Return copy of transaction history."""
return self.transaction_history.copy()
def transfer_to(self, other_account, amount):
"""Transfer money to another account."""
self.withdraw(amount)
other_account.deposit(amount)
self.transaction_history.append(f"Transferred ${amount} to {other_account.account_holder}")
other_account.transaction_history.append(f"Received ${amount} from {self.account_holder}")
# Using instance methods
alice_account = BankAccount("Alice", 1000)
bob_account = BankAccount("Bob", 500)
alice_account.deposit(200) # Alice has $1200
alice_account.withdraw(100) # Alice has $1100
alice_account.transfer_to(bob_account, 300) # Alice: $800, Bob: $800
print(alice_account.get_balance()) # 800
print(bob_account.get_balance()) # 800
print(alice_account.get_transaction_history())Special methods (also called "magic methods" or "dunder methods") define how objects behave with Python's built-in functions and operators:
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
"""Human-readable representation (for print(), str())."""
return f"'{self.title}' by {self.author}"
def __repr__(self):
"""Developer representation (for debugging, repr())."""
return f"Book(title='{self.title}', author='{self.author}', pages={self.pages})"
book = Book("1984", "George Orwell", 328)
print(book) # Uses __str__: '1984' by George Orwell
print(repr(book)) # Uses __repr__: Book(title='1984', author='George Orwell', pages=328)
# If __str__ is not defined, __repr__ is used as fallback
print(str(book)) # '1984' by George Orwellclass Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
def __eq__(self, other):
"""Define equality comparison."""
if not isinstance(other, Student):
return NotImplemented
return self.gpa == other.gpa
def __lt__(self, other):
"""Define less-than comparison."""
if not isinstance(other, Student):
return NotImplemented
return self.gpa < other.gpa
def __le__(self, other):
"""Define less-than-or-equal comparison."""
return self < other or self == other
def __str__(self):
return f"{self.name} (GPA: {self.gpa})"
alice = Student("Alice", 3.8)
bob = Student("Bob", 3.5)
charlie = Student("Charlie", 3.8)
print(alice == charlie) # True (same GPA)
print(alice == bob) # False
print(bob < alice) # True
print(alice <= charlie) # True
# Can now sort lists of students
students = [alice, bob, charlie]
students.sort() # Sorts by GPA using __lt__
print([str(s) for s in students]) # Bob comes first (lowest GPA)class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __len__(self):
"""Support len() function."""
return len(self.songs)
def __getitem__(self, index):
"""Support indexing and iteration."""
return self.songs[index]
def __contains__(self, song):
"""Support 'in' operator."""
return song in self.songs
def __str__(self):
return f"Playlist '{self.name}' with {len(self)} songs"
playlist = Playlist("Rock Classics")
playlist.add_song("Bohemian Rhapsody")
playlist.add_song("Stairway to Heaven")
playlist.add_song("Hotel California")
print(len(playlist)) # 3
print(playlist[0]) # Bohemian Rhapsody
print("Hotel California" in playlist) # True
# Can iterate because of __getitem__
for song in playlist:
print(f"♪ {song}")Regular methods that operate on individual objects:
class Calculator:
def __init__(self):
self.result = 0
def add(self, value): # Instance method
"""Add value to current result."""
self.result += value
return self
def multiply(self, value): # Instance method
"""Multiply current result by value."""
self.result *= value
return self
def get_result(self): # Instance method
"""Get current result."""
return self.result
calc = Calculator()
calc.add(5).multiply(3) # Method chaining works because methods return self
print(calc.get_result()) # 15Methods that operate on the class itself, not instances:
class Person:
population = 0
def __init__(self, name, birth_year):
self.name = name
self.birth_year = birth_year
Person.population += 1
@classmethod
def get_population(cls):
"""Class method to get total population."""
return cls.population
@classmethod
def from_age(cls, name, age):
"""Alternative constructor that takes age instead of birth year."""
from datetime import datetime
birth_year = datetime.now().year - age
return cls(name, birth_year) # Create instance using regular constructor
def get_age(self):
"""Instance method to get person's age."""
from datetime import datetime
return datetime.now().year - self.birth_year
# Regular constructor
person1 = Person("Alice", 1990)
# Alternative constructor (class method)
person2 = Person.from_age("Bob", 25)
print(Person.get_population()) # 2
print(person2.get_age()) # 25 (approximately)Methods that belong to the class but don't access class or instance data:
class MathUtils:
@staticmethod
def is_prime(n):
"""Check if a number is prime."""
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
@staticmethod
def factorial(n):
"""Calculate factorial of n."""
if n <= 1:
return 1
return n * MathUtils.factorial(n - 1)
# Can call on class or instance
print(MathUtils.is_prime(17)) # True
print(MathUtils.factorial(5)) # 120
# Also works with instance, but not common
utils = MathUtils()
print(utils.is_prime(15)) # FalseProperties provide a way to use method syntax while maintaining attribute-like access:
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius # Private attribute
@property
def celsius(self):
"""Get temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set temperature in Celsius with validation."""
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
"""Get temperature in Fahrenheit."""
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature using Fahrenheit."""
self.celsius = (value - 32) * 5/9 # Uses celsius setter for validation
@property
def kelvin(self):
"""Get temperature in Kelvin."""
return self._celsius + 273.15
@kelvin.setter
def kelvin(self, value):
"""Set temperature using Kelvin."""
self.celsius = value - 273.15 # Uses celsius setter for validation
temp = Temperature(25) # 25°C
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
print(temp.kelvin) # 298.15
# Setting temperature using different scales
temp.fahrenheit = 100
print(temp.celsius) # 37.77777777777778
temp.kelvin = 300
print(temp.celsius) # 26.85Python doesn't have true private attributes, but uses naming conventions to indicate intended usage:
class BankAccount:
def __init__(self, account_number, initial_balance):
self.account_number = account_number # Public
self._balance = initial_balance # Protected (internal use)
self.__pin = self._generate_pin() # Private (name mangled)
def _generate_pin(self):
"""Protected method - intended for internal use."""
import random
return random.randint(1000, 9999)
def __encrypt_data(self, data):
"""Private method - name mangled."""
return f"encrypted_{data}"
def deposit(self, amount):
"""Public method."""
if amount > 0:
self._balance += amount
return self._balance
raise ValueError("Amount must be positive")
def get_balance(self):
"""Public method to access protected attribute."""
return self._balance
def verify_pin(self, pin):
"""Public method to verify PIN."""
return pin == self.__pin
account = BankAccount("12345", 1000)
print(account.account_number) # OK - public
print(account._balance) # Discouraged but works - protected
# print(account.__pin) # AttributeError - private
print(account.get_balance()) # 1000 - use public interface
print(account.verify_pin(account._BankAccount__pin)) # True - name manglingclass Point:
"""Represents a point in 2D space."""
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def distance_from_origin(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def distance_from(self, other):
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
def __str__(self):
return f"Point({self.x}, {self.y})"
p1 = Point(3, 4)
p2 = Point(0, 0)
print(p1.distance_from_origin()) # 5.0
print(p1.distance_from(p2)) # 5.0class TrafficLight:
"""Models a traffic light with state changes."""
def __init__(self):
self._state = "red"
self._states = ["red", "green", "yellow"]
@property
def state(self):
return self._state
def next_state(self):
"""Advance to next state in cycle."""
current_index = self._states.index(self._state)
next_index = (current_index + 1) % len(self._states)
self._state = self._states[next_index]
def can_go(self):
"""Check if vehicles can proceed."""
return self._state == "green"
def should_slow(self):
"""Check if vehicles should slow down."""
return self._state == "yellow"
light = TrafficLight()
print(light.state) # red
print(light.can_go()) # False
light.next_state()
print(light.state) # green
print(light.can_go()) # Trueclass Pizza:
"""Pizza with customizable toppings."""
def __init__(self, size):
self.size = size
self.toppings = []
self.crust = "regular"
self.sauce = "tomato"
def add_topping(self, topping):
"""Add a topping and return self for method chaining."""
self.toppings.append(topping)
return self
def set_crust(self, crust):
"""Set crust type and return self for method chaining."""
self.crust = crust
return self
def set_sauce(self, sauce):
"""Set sauce type and return self for method chaining."""
self.sauce = sauce
return self
def __str__(self):
return f"{self.size} pizza with {self.crust} crust, {self.sauce} sauce, and {', '.join(self.toppings)}"
# Method chaining for fluent interface
pizza = (Pizza("large")
.set_crust("thin")
.set_sauce("pesto")
.add_topping("mozzarella")
.add_topping("mushrooms")
.add_topping("pepperoni"))
print(pizza) # large pizza with thin crust, pesto sauce, and mozzarella, mushrooms, pepperoniclass Rectangle:
def __init__(self, width, height):
# Validate inputs early
if not isinstance(width, (int, float)) or not isinstance(height, (int, float)):
raise TypeError("Width and height must be numbers")
if width <= 0 or height <= 0:
raise ValueError("Width and height must be positive")
self.width = width
self.height = height
def get_area(self):
return self.width * self.heightclass StudentGrades:
def __init__(self, student_name):
self.student_name = student_name
self._grades = []
def add_grade(self, grade):
"""Add a grade with validation."""
if not isinstance(grade, (int, float)):
raise TypeError("Grade must be a number")
if not 0 <= grade <= 100:
raise ValueError("Grade must be between 0 and 100")
self._grades.append(grade)
def get_grades(self):
"""Return a copy of grades to prevent external modification."""
return self._grades.copy()
def get_average(self):
"""Calculate average grade."""
if not self._grades:
return 0
return sum(self._grades) / len(self._grades)class FileManager:
"""Context manager for file operations."""
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
"""Called when entering 'with' block."""
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
"""Called when exiting 'with' block."""
if self.file:
self.file.close()
# Usage with 'with' statement
with FileManager("example.txt", "w") as f:
f.write("Hello, world!")
# File is automatically closedAs emphasized in the python-org README, classes and objects are fundamental to organizing Python code because:
- Data Organization: Group related data and behavior together in logical units
- Code Reusability: Create templates that can be used to make multiple objects
- Abstraction: Hide implementation details behind clean interfaces
- Modularity: Break complex problems into manageable, self-contained units
- Real-world Modeling: Represent entities and concepts from your problem domain
Classes enable you to:
- Model complex systems: Users, accounts, products, games, etc.
- Manage state: Track changing data over time
- Encapsulate behavior: Keep related functions with their data
- Create APIs: Provide clean interfaces for other code to use
- Build frameworks: Create extensible systems others can build upon
Mastering classes and objects - including constructors, methods, attributes, and special methods - is essential for writing maintainable, scalable Python applications. They provide the foundation for organizing code in a way that matches how we think about problems in the real world.