Object-Oriented Programming in Python
Mastering classes, objects, and Python's OOP features
Table of Contents
1. Classes and Objects Fundamentals
Core Concepts
What is a Class?
A class is a blueprint for creating objects. It defines attributes (data) and methods (functions) that the created objects will have.
What is an Object?
An object is an instance of a class. It's a concrete "thing" created from the class blueprint, with its own set of attributes.
# Class definition
class Dog:
"""A simple Dog class"""
def bark(self):
print("Woof!")
# Object creation
my_dog = Dog()
my_dog.bark() # Output: Woof!
The self Parameter
self
refers to the instance calling the method. Python automatically passes it when calling instance methods.
Note: While self
is the convention, you can technically use any name, but don't!
Object Identity and Comparison
id() Function
Returns the memory address of an object:
a = [1, 2, 3]
print(id(a)) # Memory address like 140245123456
is vs ==
Operator | Description | Example |
---|---|---|
is |
Identity comparison (same object in memory) | a is b |
== |
Equality comparison (same value) | a == b |
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 == list2) # True (same content)
print(list1 is list2) # False (different objects)
print(list1 is list3) # True (same object)
Object Lifecycle
Python uses reference counting and garbage collection to manage memory:
- Objects are created when instantiated
- Memory is reclaimed when no references remain
- The
__del__
method is called before destruction
2. Class Definition and Structure
Basic Class Definition
class Person:
"""A class representing a person.
Attributes:
name (str): The person's name
age (int): The person's age
"""
species = "Homo sapiens" # Class attribute
def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age # Instance attribute
Naming Conventions
- Class names:
PascalCase
- Method names:
snake_case
- Private conventions:
_single_leading_underscore
(protected),__double_leading_underscore
(name mangled)
Empty Classes
Use pass
as a placeholder:
class MyEmptyClass:
pass
Advanced Class Features
Type Hints in Classes
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def move(self, dx: float, dy: float) -> 'Point':
self.x += dx
self.y += dy
return self
Abstract Base Classes (ABC)
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self) -> float:
return 3.14 * self.radius ** 2
Class Decorators
from dataclasses import dataclass
from typing import final
@dataclass
class Point:
x: float
y: float
@final
class CannotBeInherited:
pass
3. Attributes and Methods
Instance Attributes
Adding Attributes Dynamically
class Dog:
pass
d = Dog()
d.name = "Fido" # Added dynamically
d.age = 3
__dict__ Attribute
Contains the instance's namespace dictionary:
print(d.__dict__) # {'name': 'Fido', 'age': 3}
Attribute Access Order
Python searches for attributes in this order:
- Instance attributes
- Class attributes
- Parent class attributes (inheritance)
Method Types
Method Type | Description | Example |
---|---|---|
Instance Method | Receives instance as first arg (self ) |
def method(self, args): |
Class Method (@classmethod ) |
Receives class as first arg (cls ) |
@classmethod |
Static Method (@staticmethod ) |
Receives no special first arg | @staticmethod |
Property Method (@property ) |
Defines getter for computed attribute | @property |
class MyClass:
@classmethod
def class_method(cls):
print(f"Called class_method of {cls}")
@staticmethod
def static_method():
print("Called static_method")
@property
def computed_prop(self):
return "Computed value"
4. Constructors and Initialization
__init__ Method
The constructor method called when an instance is created:
class Person:
def __init__(self, name, age=18): # Default age
self.name = name
self.age = age
p = Person("Alice", 25)
Calling Parent Constructors
class Employee(Person):
def __init__(self, name, age, employee_id):
super().__init__(name, age) # Call parent __init__
self.employee_id = employee_id
Other Special Methods
__new__ Method
Controls instance creation (rarely used directly):
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
Object Representation Methods
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point at ({self.x}, {self.y})"
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
p = Point(3, 4)
print(str(p)) # Point at (3, 4)
print(repr(p)) # Point(x=3, y=4)
5. Instance vs Class Variables
Instance Variables
Unique to each instance, defined in __init__
:
class Dog:
def __init__(self, name):
self.name = name # Instance variable
d1 = Dog("Fido")
d2 = Dog("Buddy")
print(d1.name, d2.name) # Fido Buddy
Class Variables
Shared among all instances, defined in class body:
class Dog:
species = "Canis familiaris" # Class variable
def __init__(self, name):
self.name = name
d1 = Dog("Fido")
d2 = Dog("Buddy")
print(d1.species, d2.species) # Same for both
Dog.species = "Canis lupus" # Changes all instances
print(d1.species) # Canis lupus
Warning: Be careful with mutable class variables as they're shared across all instances!
Common Use Cases
- Constants (e.g.,
PI = 3.14159
) - Counters (e.g., instance counting)
- Default values
6. Advanced OOP Concepts
Name Mangling (__var)
Python adds class name prefix to "private" attributes:
class MyClass:
def __init__(self):
self.__private = "secret" # Name mangled to _MyClass__private
obj = MyClass()
print(obj.__dict__) # {'_MyClass__private': 'secret'}
Property Decorators
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius"""
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@radius.deleter
def radius(self):
print("Deleting radius")
del self._radius
c = Circle(5)
print(c.radius) # Uses getter
c.radius = 10 # Uses setter
del c.radius # Uses deleter
Class Composition
"Has-a" relationship (alternative to inheritance):
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self):
self.engine = Engine() # Composition
def start(self):
self.engine.start()
my_car = Car()
my_car.start() # Engine started
7. Practical Patterns and Best Practices
Common Patterns
Singleton Pattern
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
a = Singleton()
b = Singleton()
print(a is b) # True
Factory Method
class ShapeFactory:
@staticmethod
def create_shape(shape_type):
if shape_type == "circle":
return Circle()
elif shape_type == "square":
return Square()
raise ValueError("Invalid shape type")
shape = ShapeFactory.create_shape("circle")
Performance Considerations
__slots__ Optimization
Reduces memory usage by preventing dynamic attribute creation:
class Point:
__slots__ = ['x', 'y'] # Only these attributes allowed
def __init__(self, x, y):
self.x = x
self.y = y
Method Resolution Order (MRO)
Determines method lookup order in inheritance:
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
super().method()
class C(A):
def method(self):
print("C")
super().method()
class D(B, C):
def method(self):
print("D")
super().method()
d = D()
d.method()
# Output:
# D
# B
# C
# A
print(D.__mro__) # Shows method resolution order
8. Debugging and Introspection
Inspection Tools
type() and isinstance()
class Animal: pass
class Dog(Animal): pass
d = Dog()
print(type(d)) # <class '__main__.Dog'>
print(isinstance(d, Dog)) # True
print(isinstance(d, Animal)) # True (inheritance)
dir() Function
Lists attributes and methods of an object:
print(dir(d)) # ['__class__', '__delattr__', ...]
inspect Module
import inspect
print(inspect.getmembers(d)) # Detailed member info
print(inspect.getsource(Dog)) # Shows class source code
Common Pitfalls
Mutable Default Arguments
class BadIdea:
def __init__(self, items=[]): # Shared across instances!
self.items = items
a = BadIdea()
a.items.append(1)
b = BadIdea()
print(b.items) # [1] - Surprise!
Accidental Class Variable Sharing
class Dog:
tricks = [] # Shared by all dogs!
def add_trick(self, trick):
self.tricks.append(trick)
d1 = Dog()
d2 = Dog()
d1.add_trick("roll over")
print(d2.tricks) # ['roll over'] - Oops!
Complete Code Example
class Person:
"""A class representing a person"""
species = "Homo sapiens" # Class variable
def __init__(self, name: str, age: int):
self.name = name # Instance variable
self.age = age # Instance variable
@classmethod
def from_birth_year(cls, name: str, birth_year: int) -> 'Person':
"""Alternative constructor"""
return cls(name, 2023 - birth_year)
@property
def is_adult(self) -> bool:
"""Computed property"""
return self.age >= 18
def __str__(self) -> str:
return f"{self.name} ({self.age})"
def __repr__(self) -> str:
return f"Person(name='{self.name}', age={self.age})"
# Usage
p1 = Person("Alice", 25)
p2 = Person.from_birth_year("Bob", 1990)
print(p1) # Alice (25)
print(repr(p2)) # Person(name='Bob', age=33)
print(p1.is_adult) # True