# I Struggled with OOP in Java. Python Made It Click.

Let me be honest with you.

When I first encountered Object Oriented Programming in my 4th semester, it was taught in Java. I struggled through the whole course. Never understood the *why*, never understood the *what* — so the *how* never stuck. I 'by-hearted' it, showed up for evaluations, wrote something, got some marks, and moved on.

That's not learning. That's surviving a semester.

It was in my 5th semester, when I started learning Python properly and following structured courses, that OOP finally started making sense. The *why* became clear. And once the why is clear, everything else follows.

This blog is what I wish someone had told me before that 4th semester. Not a dry textbook explanation. Just an honest breakdown of what OOP actually is, why it exists, and how it makes your code cleaner and more powerful.

* * *

## So what actually is OOP?

Here's my definition — not the textbook one:

> **OOP lets you build your own custom data types.**

That's it at its core. You define how your data type stores data, and what operations you can perform on that data. You're not just using `int`, `float`, `list` — you're creating your own types that model real world things.

Python's built-in types are a great example of this already at work. A `list` stores data in a certain way and gives you methods like `.append()`, `.pop()`, `.sort()`. Someone designed that. OOP is you doing the same thing — designing your own types for your own problems.

* * *

## Let's build something — Shapes

Say you're writing a program that works with geometric shapes. Without OOP, you'd be writing loose variables and functions everywhere:

```python
# Without OOP — messy, doesn't scale
radius = 5
area = 3.14159 * radius ** 2
circumference = 2 * 3.14159 * radius

length = 4
breadth = 6
rect_area = length * breadth
rect_perimeter = 2 * (length + breadth)
```

Now imagine 10 shapes. Or 100. This gets out of hand fast.

With OOP, you define a `Circle` type once — how it stores data, what it can do — and then just create as many circles as you need:

```python
import math

class Circle:
    def __init__(self, radius):   # __init__ runs automatically when you create an object
        self.radius = radius      # self refers to the specific object being created

    def area(self):
        return math.pi * self.radius ** 2

    def circumference(self):
        return 2 * math.pi * self.radius


class Rectangle:
    def __init__(self, length, breadth):
        self.length = length
        self.breadth = breadth

    def area(self):
        return self.length * self.breadth

    def perimeter(self):
        return 2 * (self.length + self.breadth)
```

Now using them is clean:

```python
c = Circle(5)              # creates a Circle object with radius 5
print(c.area())            # 78.53
print(c.circumference())   # 31.41

r = Rectangle(4, 6)        # creates a Rectangle object
print(r.area())            # 24
print(r.perimeter())       # 20
```

Notice something — both `Circle` and `Rectangle` have an `area()` method. Same name, different behavior. That's already a hint at one of the pillars we'll cover shortly.

* * *

## A more powerful example — Projectile Motion

Let's take this further with something from physics. A classic problem: a ball thrown at an angle. You want to know its range, maximum height, and time of flight.

Without OOP, every time you solve this you'd rewrite the same formulas with different numbers. With OOP, you define a `Projectile` once:

```python
import math

class Projectile:
    def __init__(self, velocity, angle_degrees):
        self.velocity = velocity
        self.angle = math.radians(angle_degrees)  # converting degrees to radians for math functions
        self.g = 9.8                               # acceleration due to gravity (m/s²)

    def time_of_flight(self):
        # total time the projectile stays in the air
        # formula: t = 2 * v * sin(θ) / g
        return (2 * self.velocity * math.sin(self.angle)) / self.g

    def max_height(self):
        # highest point the projectile reaches
        # formula: h = v² * sin²(θ) / 2g
        return (self.velocity ** 2 * math.sin(self.angle) ** 2) / (2 * self.g)

    def range(self):
        # horizontal distance covered
        # formula: R = v² * sin(2θ) / g
        return (self.velocity ** 2 * math.sin(2 * self.angle)) / self.g
```

Now solving for any projectile is just two lines:

```python
ball = Projectile(velocity=30, angle_degrees=45)
print(f"Time of flight : {ball.time_of_flight():.2f} s")   # :.2f means 2 decimal places
print(f"Max height     : {ball.max_height():.2f} m")
print(f"Range          : {ball.range():.2f} m")

rocket = Projectile(velocity=100, angle_degrees=60)         # same class, different values
print(f"Rocket range   : {rocket.range():.2f} m")
```

The formulas are written once. The data lives with the object. You just plug in different values and get answers. That's the power of bundling data and operations together.

* * *

## The 4 Pillars of OOP

Now that you've seen OOP in action, the 4 pillars will make much more sense.

### 1\. Encapsulation

Wrapping data and methods together into a class — that's encapsulation. But it goes further. You can also *restrict* direct access to certain attributes and instead provide controlled methods to access or modify them.

```python
class StudentRecord:
    def __init__(self, name, marks):
        self.name = name
        self.__marks = marks    # double underscore makes it private — can't access directly from outside

    def update_marks(self, new_marks):
        if 0 <= new_marks <= 100:           # validation — only valid marks allowed
            self.__marks = new_marks
        else:
            print("Invalid marks. Must be between 0 and 100.")

    def get_marks(self):
        return self.__marks                 # controlled way to read the data

s = StudentRecord("Shashank", 85)
s.update_marks(92)
print(s.get_marks())        # 92
s.update_marks(150)         # Invalid marks. Must be between 0 and 100.
# print(s.__marks)          # Error — restricted, can't access directly
```

Why does this matter? Because now you control *how* the data is accessed and modified. Direct access has no such protection — anyone could set marks to 150 or -10 and your program wouldn't know.

* * *

### 2\. Abstraction

This is, in my opinion, the fundamental *motive* behind OOP.

When you call `ball.range()`, you don't need to think about the formula inside. When you call `list.sort()`, you don't need to know which sorting algorithm Python uses internally. The complexity is hidden. Only what's relevant to the user is visible.

You use abstraction every day without realising it — every time you call a method without caring how it works inside, that's abstraction doing its job.

```python
# You don't need to know how area() works internally
# You just know it exists and what it gives you
c = Circle(7)
print(c.area())     # just works
```

* * *

### 3\. Inheritance

If two classes share mostly the same data and operations, why write it twice? Let one inherit from the other.

```python
class Shape:
    def __init__(self, color, filled):
        self.color = color
        self.filled = filled        # True if the shape is filled, False if just outline

    def info(self):
        status = "filled" if self.filled else "not filled"
        print(f"Color: {self.color}, {status}")


class Circle(Shape):                # Circle inherits everything from Shape
    def __init__(self, color, filled, radius):
        super().__init__(color, filled)   # super() calls Shape's __init__ — no need to rewrite it
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2


class Rectangle(Shape):             # Rectangle also inherits from Shape
    def __init__(self, color, filled, length, breadth):
        super().__init__(color, filled)
        self.length = length
        self.breadth = breadth

    def area(self):
        return self.length * self.breadth


c = Circle("red", True, 5)
c.info()            # Color: red, filled  ← inherited from Shape
print(c.area())     # 78.53              ← Circle's own method

r = Rectangle("blue", False, 4, 6)
r.info()            # Color: blue, not filled  ← same inherited method
print(r.area())     # 24
```

`Circle` and `Rectangle` both get `info()` for free from `Shape`. You only write what's new or different in each child class.

* * *

### 4\. Polymorphism

Same method name, different behavior depending on the object. No conflicts, because each method is wrapped inside its own class.

```python
shapes = [Circle("red", True, 5), Rectangle("blue", False, 4, 6)]

for shape in shapes:
    print(shape.area())     # Python automatically calls the right area() for each object
```

`Circle.area()` and `Rectangle.area()` have the same name but do completely different things. Python knows which one to call based on the object. That's polymorphism — many forms, one interface.

* * *

## Why does any of this matter?

When you use pandas for data analysis, you load a CSV and get back a `DataFrame` — an object of a pandas class, with hundreds of methods wrapped around it. You don't think about how any of it works internally. You just call `.groupby()` or `.fillna()` and it works. That's abstraction doing its job.

And it's not just pandas. Right from data analysis libraries to deep learning frameworks like PyTorch, to LLM orchestration tools like LangChain and LangGraph — everything is objects. All of it is well designed classes.

OOP is not a concept you learn once for an exam. It's the foundation everything you'll use is actually built on.

While learning this in Java, I never really saw the applications — that could just be my experience. With Python, I got to actually see how the tools I was already using were built on the same concepts. That's what made the difference. That's what made it click.

* * *

*I'm Shashank, a pre-final year CS student learning Python, data science, and AI. Writing about everything as I build and learn.*
