CODE: Python Classes and OOP

Python supports Object-Oriented Programming (OOP), which allows you to structure code around objects — combining data (attributes) and behavior (methods).

What is a Class?

  • class is a blueprint for creating objects.
  • It defines attributes (variables) and methods (functions).

python
class Car:
    # class attribute
    wheels = 4  

    # constructor (initializer)
    def __init__(self, brand, model):
        self.brand = brand     # instance attribute
        self.model = model

    # method (behavior)
    def drive(self):
        print(f"{self.brand} {self.model} is driving...")


What is an Object?

  • An object is an instance of a class.
  • You create objects by calling the class like a function.

python
# Create objects
car1 = Car("Tesla", "Model S")
car2 = Car("BMW", "X5")

# Access attributes
print(car1.brand)   # Tesla
print(car2.model)   # X5

# Call methods
car1.drive()        # Tesla Model S is driving...


The `__init__` Method

  • A special method called when a new object is created.
  • Used to initialize attributes.

python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Person("Amr", 30)
print(p.name, p.age)   # Amr 30


Instance vs Class Attributes

  • Instance attributes → belong to a specific object (self.attribute).
  • Class attributes → shared across all instances.

python
class Dog:
    species = "Canine"   # class attribute

    def __init__(self, name):
        self.name = name   # instance attribute

dog1 = Dog("Rex")
dog2 = Dog("Buddy")

print(dog1.species, dog1.name)  # Canine Rex
print(dog2.species, dog2.name)  # Canine Buddy


Methods

  • Functions inside classes are called methods.
  • They always take self as the first parameter (reference to the object).

python
class Calculator:
    def add(self, a, b):
        return a + b

calc = Calculator()
print(calc.add(5, 10))   # 15


Special Methods (Magic / Dunder Methods)

Python classes have special methods that begin and end with __.

  • __init__ → constructor
  • __str__ → string representation
  • __len__ → length of object (if applicable)
  • __add__ → behavior for + operator

python
class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages
    
    def __str__(self):
        return f"Book: {self.title}"

    def __len__(self):
        return self.pages

b = Book("Python Basics", 300)
print(b)           # Book: Python Basics
print(len(b))      # 300


Multiple Objects

You can create multiple independent objects from the same class.

python
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

s1 = Student("Ali", "A")
s2 = Student("Sara", "B")

print(s1.name, s1.grade)   # Ali A
print(s2.name, s2.grade)   # Sara B


Attributes and Methods

In Python’s Object-Oriented Programming (OOP), attributes and methods are the core building blocks of classes and objects. They represent the data (attributes) and the behavior (methods) of an object.


1. Attributes

Attributes are variables inside a class that hold data about an object.

Types of Attributes

  1. Instance Attributes
  • Belong to each object (instance).
  • Defined inside __init__.
  • Each object can have different values.

python
class Person:
    def __init__(self, name, age):
        self.name = name      # instance attribute
        self.age = age

p1 = Person("Amr", 30)
p2 = Person("Sara", 25)

print(p1.name, p1.age)   # Amr 30
print(p2.name, p2.age)   # Sara 25

  1. Class Attributes
  • Shared by all objects of the class.
  • Defined directly inside the class, not in __init__.

python
class Dog:
    species = "Canine"   # class attribute

    def __init__(self, name):
        self.name = name  # instance attribute

d1 = Dog("Rex")
d2 = Dog("Buddy")

print(d1.name, d1.species)  # Rex Canine
print(d2.name, d2.species)  # Buddy Canine


Methods

Methods are functions inside classes that define behaviors.

  • Always have self as the first parameter → refers to the current object.

python
class Calculator:
    def add(self, x, y):   # instance method
        return x + y
    
    def multiply(self, x, y):
        return x * y

calc = Calculator()
print(calc.add(5, 3))       # 8
print(calc.multiply(4, 6))  # 24


Types of Methods

  1. Instance Methods (most common)
  • Use self.
  • Can access/modify object attributes.

python
class Student:
    def __init__(self, name):
        self.name = name
    
    def introduce(self):
        print("Hi, my name is", self.name)

  1. Class Methods (@classmethod)
  • Take cls as the first argument.
  • Work with the class itself, not an instance.

python
class Employee:
    company = "VarApps"

    @classmethod
    def change_company(cls, new_name):
        cls.company = new_name

e1 = Employee()
e2 = Employee()
Employee.change_company("VarThings")
print(e1.company, e2.company)  # VarThings VarThings

  1. Static Methods (@staticmethod)
  • Don’t take self or cls.
  • Behave like normal functions but live inside a class.

python
class MathUtils:
    @staticmethod
    def square(x):
        return x * x

print(MathUtils.square(5))  # 25


Special (Dunder) Methods

Python has special methods that start and end with __.

They customize object behavior.

  • __init__ → constructor
  • __str__ → string representation
  • __len__ → defines behavior for len()
  • __add__ → defines behavior for +

python
class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __str__(self):
        return f"Book: {self.title}"

    def __len__(self):
        return self.pages

b = Book("Python Basics", 300)
print(b)        # Book: Python Basics
print(len(b))   # 300


Accessing and Modifying Attributes

  • Access attributes with dot notation.
  • Modify them dynamically.

python
class Car:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model

car = Car("Tesla", "Model 3")
print(car.brand)   # Tesla

car.brand = "BMW"  # modify
print(car.brand)   # BMW


Special (Magic/Dunder) Methods

In Python, many behaviors of objects are controlled by special methods, also called dunder methods (short for “double underscore”).

These methods let you customize how your objects behave with built-in functions, operators, and syntax.


What are Dunder Methods?

  • Dunder = double underscore (e.g., __init____str__).
  • They are automatically called by Python in specific situations.
  • Allow your classes to behave more like built-in types.

Example:

python
class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages
    
    def __str__(self):
        return f"Book: {self.title}, {self.pages} pages"

b = Book("Python Basics", 300)
print(b)   # Calls __str__ → Book: Python Basics, 300 pages


Commonly Used Special Methods

Object Creation and Initialization

  • __init__(self, …) → called when an object is created.
  • __new__(cls, …) → controls object creation (rarely used directly).

python
class Person:
    def __init__(self, name):
        self.name = name


String Representation

  • __str__ → user-friendly string (used by print()).
  • __repr__ → official representation (used in debugging, REPL).

python
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    
    def __str__(self):
        return f"Point({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

p = Point(3, 4)
print(str(p))   # Point(3, 4)
print(repr(p))  # Point(x=3, y=4)


Arithmetic Operators

You can redefine how operators (+-*, etc.) work on your objects.

  • __add__ → +
  • __sub__ → -
  • __mul__ → *
  • __truediv__ → /

python
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)   # Vector(4, 6)


Comparisons

  • __eq__ → ==
  • __lt__ → <
  • __le__ → <=
  • __gt__ → >
  • __ge__ → >=

python
class Box:
    def __init__(self, volume):
        self.volume = volume
    
    def __eq__(self, other):
        return self.volume == other.volume

b1 = Box(100)
b2 = Box(100)
print(b1 == b2)   # True


Length, Iteration, Indexing

  • __len__ → for len(obj).
  • __getitem__ → access with obj[key].
  • __setitem__ → assign with obj[key] = value.
  • __iter__ & __next__ → make an object iterable.

python
class MyList:
    def __init__(self, items):
        self.items = items
    
    def __len__(self):
        return len(self.items)
    
    def __getitem__(self, index):
        return self.items[index]

lst = MyList([10, 20, 30])
print(len(lst))    # 3
print(lst[1])      # 20


Context Managers

  • __enter__ and __exit__ → allow with obj: usage.

python
class FileManager:
    def __init__(self, filename):
        self.filename = filename
    
    def __enter__(self):
        self.file = open(self.filename, "w")
        return self.file
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()

with FileManager("test.txt") as f:
    f.write("Hello, dunder methods!")


3. Why Use Dunder Methods?

  • Make classes behave like native Python objects.
  • Improve readability (__str__ and __repr__).
  • Enable operator overloading (+==, etc.).
  • Support built-in functions (leniterwith, etc.).

Object-Oriented Programming

Object-Oriented Programming (OOP) in Python is built on four pillars:

  1. Encapsulation
  2. Inheritance
  3. Polymorphism
  4. Abstraction

Let’s go through them one by one with examples.


Encapsulation

Encapsulation means bundling data (attributes) and behavior (methods) into a class, and controlling how they are accessed.

Public, Protected, Private Attributes

  • Public: accessible everywhere (default).
  • Protected: prefix with _, used internally (convention).
  • Private: prefix with __, not directly accessible (name mangling).

python
class Account:
    def __init__(self, owner, balance):
        self.owner = owner        # public
        self._status = "Active"   # protected
        self.__balance = balance  # private
    
    def deposit(self, amount):
        self.__balance += amount
    
    def get_balance(self):
        return self.__balance

acc = Account("Amr", 1000)
print(acc.owner)         # Amr
print(acc._status)       # Should be treated as internal
# print(acc.__balance)   # Error
print(acc.get_balance()) # Safe access


Inheritance

Inheritance allows a class (child) to reuse and extend another class’s (parent) attributes and methods.

python
class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        print("Some sound")

class Dog(Animal):
    def speak(self):
        print(f"{self.name} says Woof!")

class Cat(Animal):
    def speak(self):
        print(f"{self.name} says Meow!")

dog = Dog("Rex")
cat = Cat("Mimi")

dog.speak()  # Rex says Woof!
cat.speak()  # Mimi says Meow!

Benefits: code reuse, hierarchy, easier maintenance.

Polymorphism

Polymorphism means “many forms” — the same method name can have different implementations depending on the object.

Example with Different Classes

python
class Bird:
    def make_sound(self):
        print("Chirp")

class Dog:
    def make_sound(self):
        print("Woof")

animals = [Bird(), Dog()]
for animal in animals:
    animal.make_sound()

Example with Inheritance

python
class Shape:
    def area(self):
        return 0

class Circle(Shape):
    def __init__(self, r):
        self.r = r
    def area(self):
        return 3.14 * self.r * self.r

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side * self.side

shapes = [Circle(5), Square(4)]
for s in shapes:
    print(s.area())  # 78.5, 16


Abstraction

Abstraction means hiding unnecessary details and showing only the essential features of an object.

In Python, this is often done with abstract base classes (ABC).

  • An abstract class cannot be instantiated.
  • It defines abstract methods that must be implemented in child classes.

python
from abc import ABC, abstractmethod

class Vehicle(ABC):   # abstract base class
    @abstractmethod
    def start(self):
        pass

class Car(Vehicle):
    def start(self):
        print("Car engine started")

class Bike(Vehicle):
    def start(self):
        print("Bike started with a kick")

# vehicle = Vehicle()   # Error: can't instantiate abstract class
car = Car()
bike = Bike()

car.start()   # Car engine started
bike.start()  # Bike started with a kick

Benefits: ensures **consistent interfaces** for child classes.

Summary

  • Encapsulation → bundles data & behavior, controls access (public/protected/private).
  • Inheritance → reuse and extend code across classes.
  • Polymorphism → same method name, different behaviors.
  • Abstraction → define abstract interfaces, hide unnecessary details.

Together, these four pillars make Python’s OOP powerful, modular, and maintainable.