Object-Oriented Programming
in Python
Theory + Practical Code — From Basics to Advanced
A concise, code-driven guide covering classes, objects, the four pillars of OOP, dunder methods, and
advanced patterns — with runnable Python examples throughout.
Contents
• 1. What is OOP? (Theory)
• 2. Classes and Objects
• 3. The __init__ Method and Attributes
• 4. Instance vs Class Attributes/Methods
• 5. Encapsulation
• 6. Inheritance
• 7. Polymorphism
• 8. Abstraction
• 9. Dunder (Magic) Methods
• 10. Composition vs Inheritance
• 11. Advanced: Property Decorators
• 12. Advanced: Abstract Base Classes
• 13. Advanced: Class Design Principles (SOLID, brief)
• 14. Quick Reference Cheat Sheet
1. What is OOP?
Object-Oriented Programming (OOP) is a way of structuring code around objects — bundles of data
(attributes) and behavior (methods) — instead of just functions acting on raw data. Python is a fully
object-oriented language: even integers, strings, and functions are objects.
The Four Pillars
• Encapsulation — bundling data and methods together, and controlling access to internals.
• Inheritance — creating new classes that reuse and extend existing ones.
• Polymorphism — using a single interface for different underlying data types/classes.
• Abstraction — hiding complex implementation details behind a simple interface.
■ Why bother? OOP models real-world relationships naturally, reduces code duplication, and makes large
codebases easier to extend and maintain.
2. Classes and Objects
A class is a blueprint. An object (or instance) is a concrete thing built from that blueprint. Think: class = 'Car'
design, object = your actual car.
class Dog:
pass # empty class body
my_dog = Dog() # my_dog is an object (instance) of Dog
print(type(my_dog)) # <class '__main__.Dog'>
print(isinstance(my_dog, Dog)) # True
3. The __init__ Method and Attributes
__init__ is the constructor — it runs automatically when an object is created, and is used to set up initial
attributes (data stored on the object). self refers to the specific instance being created/used.
class Dog:
def __init__(self, name, breed):
[Link] = name # instance attribute
[Link] = breed
def bark(self): # instance method
return f"{[Link]} says Woof!"
rex = Dog("Rex", "Labrador")
print([Link]) # Rex
print([Link]()) # Rex says Woof!
4. Instance vs Class Attributes/Methods
Class attributes are shared by all instances. Instance attributes belong to one object only. Similarly,
@classmethod operates on the class, and @staticmethod is a plain function namespaced inside the
class.
class Dog:
species = "Canis familiaris" # class attribute (shared)
def __init__(self, name):
[Link] = name # instance attribute
@classmethod
def from_string(cls, data): # alternate constructor
name = [Link]("-")[0]
return cls(name)
@staticmethod
def is_adult(age_years): # utility, no self/cls data needed
return age_years >= 1
print([Link]) # Canis familiaris
d = Dog.from_string("Buddy-3yrs")
print([Link]) # Buddy
print(Dog.is_adult(2)) # True
5. Encapsulation
Encapsulation restricts direct access to internal state. Python has no true 'private' keyword, but uses naming
conventions: _single_underscore = 'internal use, please don't touch', and __double_underscore =
name-mangled to discourage accidental access.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # name-mangled to _BankAccount__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def get_balance(self):
return self.__balance
acc = BankAccount(100)
[Link](50)
print(acc.get_balance()) # 150
# print(acc.__balance) # AttributeError - not directly accessible
6. Inheritance
Inheritance lets a child class reuse and extend a parent class. Use super() to call the parent's
implementation.
class Animal:
def __init__(self, name):
[Link] = name
def speak(self):
return f"{[Link]} makes a sound"
class Cat(Animal): # Cat inherits from Animal
def speak(self): # method overriding
return f"{[Link]} says Meow"
class Kitten(Cat): # multilevel inheritance
def speak(self):
base = super().speak() # call parent method
return base + " (softly, it's a kitten)"
a = Animal("Generic")
c = Cat("Whiskers")
k = Kitten("Tiny")
print([Link]()) # Generic makes a sound
print([Link]()) # Whiskers says Meow
print([Link]()) # Tiny says Meow (softly, it's a kitten)
■ Python also supports multiple inheritance: class C(A, B). Method Resolution Order (MRO) decides which parent's
method wins — check with ClassName.__mro__.
7. Polymorphism
Polymorphism means the same interface behaves differently depending on the object. Python supports this
naturally via duck typing: 'if it walks like a duck and quacks like a duck...' — no explicit interface required.
class Cat:
def speak(self): return "Meow"
class Dog:
def speak(self): return "Woof"
def animal_sound(animal):
print([Link]()) # works for ANY object with .speak()
for a in [Cat(), Dog()]:
animal_sound(a) # Meow, then Woof
8. Abstraction
Abstraction hides implementation detail behind a simple interface, exposing only what's necessary. In
Python this is formalized with the abc module.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
... # no implementation - forces subclasses to define it
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
# Shape() # TypeError: Can't instantiate abstract class
r = Rectangle(4, 5)
print([Link]()) # 20
9. Dunder (Magic) Methods
Double-underscore methods let your objects work with Python's built-in syntax (printing, operators, length,
iteration, etc.).
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self): # developer-facing representation
return f"Point({self.x}, {self.y})"
def __eq__(self, other): # enables == comparison
return self.x == other.x and self.y == other.y
def __add__(self, other): # enables + operator
return Point(self.x + other.x, self.y + other.y)
def __len__(self): # enables len()
return int((self.x**2 + self.y**2) ** 0.5)
p1, p2 = Point(1, 2), Point(3, 4)
print(p1 + p2) # Point(4, 6)
print(p1 == Point(1,2)) # True
print(len(p1)) # 2
10. Composition vs Inheritance
Composition builds objects out of other objects ('has-a' relationship) instead of extending a base class
('is-a'). It's often more flexible than deep inheritance chains — the common guideline is 'favor composition
over inheritance'.
class Engine:
def start(self):
return "Engine starting..."
class Car:
def __init__(self):
[Link] = Engine() # Car "has-a" Engine (composition)
def start(self):
return [Link]()
my_car = Car()
print(my_car.start()) # Engine starting...
11. Advanced: Property Decorators
@property lets you expose a method as if it were an attribute — useful for computed values or validated
setters, keeping a clean public API.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@[Link]
def radius(self, value):
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def area(self): # read-only computed property
return 3.14159 * self._radius ** 2
c = Circle(5)
print([Link]) # 78.53975
[Link] = 10 # goes through the setter's validation
print([Link]) # 314.159
12. Advanced: Abstract Base Classes & Interfaces
ABCs can define a shared contract across many unrelated classes, enabling polymorphism with
compile-time-like safety (enforced at instantiation).
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount): ...
class CreditCard(PaymentMethod):
def pay(self, amount):
return f"Charged ${amount} to credit card"
class PayPal(PaymentMethod):
def pay(self, amount):
return f"Paid ${amount} via PayPal"
def checkout(method: PaymentMethod, amount):
print([Link](amount)) # polymorphic call
checkout(CreditCard(), 50)
checkout(PayPal(), 75)
13. Advanced: Class Design Principles (Brief)
The SOLID principles guide clean OOP design in any language, including Python:
Principle Meaning
S Single Responsibility A class should have one reason to change.
O Open/Closed Open for extension, closed for modification.
L Liskov Substitution Subclasses should be usable wherever the parent is expected.
I Interface Segregation Prefer many small, specific interfaces over one large one.
D Dependency Inversion Depend on abstractions, not concrete implementations.
14. Quick Reference Cheat Sheet
Concept Keyword / Syntax
Define class class Name: ...
Constructor def __init__(self, ...):
Instance method def method(self, ...):
Class method @classmethod / def m(cls, ...):
Static method @staticmethod / def m(...):
Inheritance class Child(Parent):
Call parent method super().method()
Abstract class from abc import ABC, abstractmethod
Property (getter) @property
Property (setter) @[Link]
String repr def __repr__(self):
Operator overload def __add__(self, other):
Private-ish attribute self.__attr (name-mangled)
Summary: Start with classes/objects and __init__, layer in encapsulation, inheritance, polymorphism and
abstraction, then reach for dunder methods, properties, and ABCs as your designs grow more advanced.
Practice by rewriting a small program you already know using these patterns.