Python Basics

Object-Oriented Programming in Python

Object-oriented programming organizes a program around objects that own state and expose behavior through clear interfaces.

What is object-oriented programming in Python?

Object-oriented programming (OOP) models a system as collaborating objects. Each object combines state with the operations allowed on that state, helping larger programs keep responsibilities and change boundaries clear.

The four core OOP principles

  • Encapsulation: keep related state and behavior together and expose controlled methods or properties.
  • Abstraction: present a useful interface while hiding implementation details callers do not need.
  • Inheritance: derive a specialized class from an existing class when there is a genuine is-a relationship.
  • Polymorphism: use the same operation with different object types, each providing appropriate behavior.

How this example is designed

  1. BankAccount owns balance state and validates deposits.
  2. SavingsAccount reuses the base account contract through inheritance.
  3. SavingsAccount overrides month_end to apply interest.
  4. The loop works with every account through the same interface.
Design tip: Prefer composition when one object merely uses another. Use inheritance only when substituting the child wherever the parent is expected remains correct.

Basic example

Run code →
main.py
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = float(balance)

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def month_end(self):
        return "No interest"


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

    def month_end(self):
        self._balance *= 1 + self.interest_rate
        return "Interest applied"


accounts = [
    BankAccount("Ada", 100),
    SavingsAccount("Lin", 200, 0.05),
]
accounts[0].deposit(50)

for account in accounts:
    status = account.month_end()
    print(f"{account.owner}: {account.balance:.2f} ({status})")

Expected output

Ada: 150.00 (No interest)
Lin: 210.00 (Interest applied)

The four OOP principles in practice

These focused examples show how Python expresses encapsulation, abstraction, inheritance, and polymorphism in everyday code.

Encapsulation

Run code →

keep related state and behavior together and expose controlled methods or properties.

encapsulation.py
class BankAccount:
    def __init__(self, balance=0):
        # Double underscores prevent accidental direct access.
        self.__balance = balance

    @property
    def balance(self):
        # Expose read access without exposing mutation.
        return self.__balance

    def deposit(self, amount):
        # Keep validation next to the state it protects.
        if amount <= 0:
            raise ValueError("Amount must be positive")
        self.__balance += amount


account = BankAccount(100)
account.deposit(50)
print(account.balance)  # 150

Abstraction

Run code →

present a useful interface while hiding implementation details callers do not need.

abstraction.py
from abc import ABC, abstractmethod


class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount):
        # Callers know what pay does, not how each provider does it.
        pass


class CreditCard(PaymentMethod):
    def pay(self, amount):
        # The concrete class hides its payment implementation.
        return f"Paid ${amount:.2f} by card"


payment = CreditCard()
print(payment.pay(25))

Inheritance

Run code →

derive a specialized class from an existing class when there is a genuine is-a relationship.

inheritance.py
class Employee:
    def __init__(self, name):
        self.name = name

    def describe(self):
        return self.name


class Developer(Employee):
    def __init__(self, name, language):
        # Reuse initialization from the parent class.
        super().__init__(name)
        self.language = language

    def describe(self):
        # Extend inherited behavior instead of duplicating it.
        return f"{super().describe()} writes {self.language}"


print(Developer("Ada", "Python").describe())

Polymorphism

Run code →

use the same operation with different object types, each providing appropriate behavior.

polymorphism.py
class EmailNotification:
    def send(self, message):
        return f"Email: {message}"


class SmsNotification:
    def send(self, message):
        return f"SMS: {message}"


def notify(channels, message):
    for channel in channels:
        # The same call selects behavior from each concrete object.
        print(channel.send(message))


channels = [EmailNotification(), SmsNotification()]
notify(channels, "Order shipped")

How it works

BankAccount encapsulates its balance and exposes controlled operations. SavingsAccount inherits that interface and overrides month_end. The loop treats both objects uniformly, so the method selected at runtime depends on the concrete object.

Change the values and run the program in the CodeUtility online Python compiler without installing Python locally.

Practice exercises

Modify the runnable example with the exercises below to build understanding beyond copying the result.

  1. Change the input and predict the output before running.
  2. Handle empty or invalid input.
  3. Wrap the logic in a function and add more test cases.
Run in Python IDE →