Python Learning Guide: Object-Oriented Programming (OOP) in Python

Who this guide is for

  • Learners who already understand Python fundamentals and functions
  • Developers who want to model real-world entities with clean structure
  • Anyone preparing to build larger applications with reusable components

What you’ll learn

  • How classes and objects work in Python
  • The role of __init__ and different method types (instance, class, static)
  • How inheritance, encapsulation, and polymorphism improve design
  • When and why to use abstract base classes (abc)
  • Common OOP mistakes and how to avoid overengineering

Why this topic matters

As projects grow, plain scripts become hard to maintain. OOP helps you organize data and behavior into clear units (classes) so your code is easier to read, test, and extend.

In Python, OOP is practical rather than rigid. You can combine object-oriented design with functional patterns, then choose the simplest structure that fits your problem. Learning OOP well gives you a strong foundation for frameworks, APIs, and scalable codebases.

Core concepts

Classes, objects, and constructors

A class is a blueprint; an object is an instance of that blueprint.

class User:
	def __init__(self, username, email):
		self.username = username
		self.email = email

	def profile(self):
		return f"{self.username} <{self.email}>"


user = User("katie", "[email protected]")
print(user.profile())

Expected output:

katie <[email protected]>

__init__ runs when a new object is created. It initializes object state.

Method types and encapsulation

Python classes commonly use three method types:

  • Instance methods: operate on object data (self)
  • Class methods: operate on class-level context (cls)
  • Static methods: utility logic related to class domain but no instance/class state
class Temperature:
	scale = "Celsius"

	def __init__(self, celsius):
		self._celsius = celsius

	def to_fahrenheit(self):
		return (self._celsius * 9 / 5) + 32

	@classmethod
	def from_fahrenheit(cls, fahrenheit):
		celsius = (fahrenheit - 32) * 5 / 9
		return cls(celsius)

	@staticmethod
	def is_valid(value):
		return isinstance(value, (int, float))

The _celsius naming style signals internal attribute intent (convention-based encapsulation).

Encapsulation visibility conventions in Python:

  • Public attribute: name (intended for normal external access)
  • Protected-style attribute: _balance (convention: internal use)
  • Private-style attribute: __pin (name-mangled to reduce accidental access)
class Wallet:
	def __init__(self, owner, pin):
		self.owner = owner          # public
		self._balance = 0           # protected-style
		self.__pin = pin            # private-style

	def deposit(self, amount):
		self._balance += amount

	def check_pin(self, pin):
		return self.__pin == pin


w = Wallet("Ava", "1234")
w.deposit(100)
print(w.owner)            # public access
print(w._balance)         # possible, but convention says internal
print(w.check_pin("1234"))

Inheritance, polymorphism, and abstract base classes

Inheritance allows child classes to reuse and extend parent behavior. Polymorphism allows different classes to share a common interface.

Common inheritance forms:

  • Single inheritance: one child inherits one parent
  • Multiple inheritance: one child inherits multiple parents
  • Multilevel inheritance: class chain (Grandparent -> Parent -> Child)

Inheritance type examples:

# Single inheritance
class Animal:
	def speak(self):
		return "..."


class Dog(Animal):
	def speak(self):
		return "Woof"


# Multiple inheritance
class FlyMixin:
	def fly(self):
		return "Flying"


class SwimMixin:
	def swim(self):
		return "Swimming"


class Duck(FlyMixin, SwimMixin):
	pass


# Multilevel inheritance
class Vehicle:
	def category(self):
		return "Transport"


class Car(Vehicle):
	def wheels(self):
		return 4


class ElectricCar(Car):
	def power_source(self):
		return "Battery"


print(Dog().speak())
print(Duck().fly(), Duck().swim())
print(ElectricCar().category(), ElectricCar().wheels(), ElectricCar().power_source())
from abc import ABC, abstractmethod


class PaymentProcessor(ABC):
	@abstractmethod
	def pay(self, amount):
		pass


class CardPayment(PaymentProcessor):
	def pay(self, amount):
		return f"Paid ${amount} by card"


class WalletPayment(PaymentProcessor):
	def pay(self, amount):
		return f"Paid ${amount} by wallet"


def checkout(processor, amount):
	print(processor.pay(amount))


checkout(CardPayment(), 120)
checkout(WalletPayment(), 85)

Expected output:

Paid $120 by card
Paid $85 by wallet

This pattern keeps extension easy while preserving a consistent API.

Simple UML-style sketch (text-based):

PaymentProcessor (abstract)
├── CardPayment
└── WalletPayment

This kind of class diagram helps document relationships for teammates before implementation.

Step-by-step walkthrough

Step 1 — Create a simple class model

Start with one class that owns both data and behavior.

class BankAccount:
	def __init__(self, owner, balance=0):
		self.owner = owner
		self.balance = balance

	def deposit(self, amount):
		self.balance += amount

	def withdraw(self, amount):
		if amount > self.balance:
			raise ValueError("Insufficient balance")
		self.balance -= amount

This gives you a clean unit with clear responsibilities.

Step 2 — Add inheritance for specialized behavior

Create a subclass for savings accounts with extra rules.

class SavingsAccount(BankAccount):
	def __init__(self, owner, balance=0, interest_rate=0.03):
		super().__init__(owner, balance)
		self.interest_rate = interest_rate

	def apply_interest(self):
		self.balance += self.balance * self.interest_rate

Use super() to reuse parent initialization cleanly.

Step 3 — Introduce polymorphic interfaces

Design code that works with any class implementing the same method.

class EmailNotifier:
	def send(self, message):
		print(f"Email: {message}")


class SMSNotifier:
	def send(self, message):
		print(f"SMS: {message}")


def notify_all(notifiers, message):
	for notifier in notifiers:
		notifier.send(message)


notify_all([EmailNotifier(), SMSNotifier()], "Payment received")

This keeps business logic flexible without if/elif chains for every type.

Practical examples

Example 1 — Product inventory model

Modeling products as objects makes state updates clearer.

class Product:
	def __init__(self, name, price, stock):
		self.name = name
		self.price = price
		self.stock = stock

	def sell(self, quantity):
		if quantity > self.stock:
			raise ValueError("Not enough stock")
		self.stock -= quantity

	def __str__(self):
		return f"{self.name} | ${self.price} | stock={self.stock}"


laptop = Product("Laptop", 999, 5)
laptop.sell(2)
print(laptop)

Expected output:

Laptop | $999 | stock=3

Example 2 — Abstract report exporters

Use abstract base classes to enforce required methods.

from abc import ABC, abstractmethod


class ReportExporter(ABC):
	@abstractmethod
	def export(self, data):
		pass


class CSVExporter(ReportExporter):
	def export(self, data):
		return f"CSV export: {data}"


class JSONExporter(ReportExporter):
	def export(self, data):
		return f"JSON export: {data}"


for exporter in [CSVExporter(), JSONExporter()]:
	print(exporter.export({"users": 120}))

Expected output:

CSV export: {'users': 120}
JSON export: {'users': 120}

This pattern is useful when you support multiple output formats or backends.

Common mistakes and how to avoid them

  • Turning every script into a class too early -> Start simple; introduce classes when state + behavior naturally belong together.
  • Exposing mutable internals carelessly -> Use clear methods (deposit, withdraw) instead of uncontrolled direct changes.
  • Deep inheritance chains -> Prefer composition when inheritance becomes hard to reason about.
  • Ignoring interface consistency -> Keep method names/parameters predictable across related classes.
  • Forgetting single responsibility -> One class should do one core job well.

Quick practice

  • Build a Student class with attributes (name, scores) and methods (add_score, average_score).
  • Create Animal parent class with subclasses Dog and Cat, each implementing a speak() method.
  • Define an abstract Storage class and implement LocalStorage + CloudStorage with a shared save(data) interface.

Key takeaways

  • OOP helps structure larger Python programs through reusable, testable components.
  • __init__, method types, and encapsulation conventions are core daily tools.
  • Inheritance and polymorphism improve extensibility when used with restraint.
  • Abstract base classes help enforce contracts in multi-implementation designs.

Next step

Continue to Data Structures & Algorithms Basics. In the next guide, you will review core structures, complexity thinking, and essential sorting/searching patterns used in interviews and real systems.

No Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.