Software

What Is Object Oriented Programming (OOP)? The 4 Core Principles With Code Examples

Talha AslanTalha Aslan 18 min read 2 views

Object oriented programming is a way of building software around objects that bundle data with the behaviour that works on that data. I have managed web projects since 2012, and this way of thinking is what keeps a growing codebase from falling apart. So in this guide I walk through the four core principles with Python code examples.

Those four principles are encapsulation, inheritance, polymorphism and abstraction. I use Python because the syntax is clean and a beginner can read it without friction. That said, every idea here applies equally to Java, C#, PHP or TypeScript. I am not covering SOLID in this article; it deserves its own piece. You can browse the rest of my programming articles in the software category.

What is object oriented programming (OOP)?

Object oriented programming is a programming paradigm that models a program as a set of objects sending messages to each other. In addition, each object carries its own data, called state, plus the methods that change that data. As a result, the code splits into small parts with clear responsibilities that mirror real concepts.

Think of an online shop. A cart, a product, a customer and an order are separate ideas. OOP then asks you to describe each of them as a class. The cart knows how to add items and total them. The order, meanwhile, tracks its own status. So when a bug shows up, you know which file to open first.

The roots of the paradigm go back to the Simula language in the 1960s. Smalltalk then refined the idea, and C++ and Java brought it into the mainstream. Today, for example, Python, PHP, C#, Kotlin and Swift all support classes and objects. In other words, what you learn here survives a change of language.

What is the difference between a class and an object?

A class is a template; an object is a concrete instance built from it. Picture an architect's drawing. The drawing itself is not a house, yet you can build a hundred houses from it. Likewise, a class describes the data and behaviour; the object is the real thing living in memory.

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

    def discounted_price(self, rate):
        return self.price * (1 - rate)

pen = Product("Pen", 4)
notebook = Product("Notebook", 12)
print(notebook.discounted_price(0.25))  # 9.0

Here Product is the class. By contrast, pen and notebook are two separate objects created from it. Specifically, both share the same methods, but each keeps its own name and price. The __init__ method is the constructor that runs when Python creates the object. The self parameter also tells the method which object it works on.

If you want to check the maths by hand, my discount calculator uses the same formula. Comparing your code with the tool is a fun way to test your first class.

Why did object oriented programming appear?

In procedural code, data and functions live apart. For a small script that is fine, because nobody else touches it. As a project grows, however, hundreds of functions start changing the same shared data. As a result, predicting the impact of one change gets hard. OOP reduces that mess by placing the data next to its owner.

CriterionProcedural approachObject oriented approach
Basic unitFunctionClass and object
Where does data live?Often shared or globalInside the object that owns it
ReuseCopying functions or librariesInheritance and composition
Impact of a changeCan spread widelyTends to stay inside the class
Best fitShort scripts, one off jobsLong lived, multi person projects

This table does not say procedural code is bad. For example, writing classes for a twenty line script that cleans one CSV file is pure ceremony. On the other hand, an admin panel that needs years of maintenance gains a lot from a clear object structure.

My own observation is simple. You usually notice that a project needs objects when the third developer joins. The first two can hold the code in their heads. The newcomer, though, loses days working out which function changes which data. Classes, in other words, write that knowledge into the code itself.

What is encapsulation and what problem does it solve?

Encapsulation means hiding an object's data from the outside world and letting others reach it only through controlled methods. The point is not secrecy for its own sake. Instead, the goal is to guarantee that the object always stays in a valid state.

Consider a bank account. If anyone could edit the balance directly, someone might write minus five thousand and the system would never notice. Encapsulation, however, closes that door. Only the deposit and withdraw methods change the balance, and those methods check the rules.

In practice encapsulation gives you three benefits:

  • You can change the internals without breaking the code that uses the object.
  • You stop invalid data at one single point instead of repeating checks everywhere.
  • You find the path that changed a value much faster while debugging.

In short, encapsulation turns the object into the guardian of its own rules. Once it becomes a habit, surprises like "how did this value go negative?" become far less common.

What does encapsulation look like in a code example?

The class below keeps the balance in a field that starts with an underscore. It exposes only a read only property:

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

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

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

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

account = BankAccount("Alice")
account.deposit(500)
account.withdraw(200)
print(account.balance)  # 300

You can read balance, yet writing account.balance = 1000 raises an error because the property has no setter. The only way to change the balance runs through deposit and withdraw. So rules like "no negative amounts" live in one place.

For instance, tomorrow you might decide to load the balance from a database. Then you change only the inside of the class. Every other piece of code keeps writing account.balance as before. In short, that is the most tangible benefit of encapsulation.

You can apply the same logic to stock levels, coupons or loyalty points. A coupon class, for instance, keeps its expiry date and usage count inside and refuses to apply itself when it is no longer valid.

Does Python really have private attributes?

No, Python has no exact equivalent of the Java private keyword. The official Python tutorial states it plainly: private instance variables that only the object itself can access do not exist in Python. Instead, you rely on a naming convention.

A name with a single leading underscore, such as _balance, signals "internal detail, hands off". With a double leading underscore, Python applies name mangling and combines the attribute name with the class name. Moreover, that mechanism exists to avoid name clashes in subclasses. It is not a security feature.

class Sample:
    def __init__(self):
        self.__hidden = 42

s = Sample()
print(s._Sample__hidden)  # 42, still reachable

So in Python, encapsulation is a matter of discipline. Your team follows the underscore convention and nobody touches those fields from outside. In Java or C#, by contrast, the compiler enforces the rule. Both approaches share one aim: drawing a clear boundary around the internal state of an object.

What is inheritance and when should you use it?

Inheritance lets one class take over the attributes and methods of another. The class that inherits is the subclass; the one it inherits from is the superclass. The subclass does not rewrite shared behaviour. Instead, it adds or changes only what makes it different.

There is a simple test for correct use. First, can you say "is a" between the two concepts? A cat is an animal, so a Cat class that extends Animal makes sense. A car, however, is not an engine; a car has an engine. In that second case inheritance is the wrong tool.

Typical situations where inheritance fits well:

  • Several classes share the same fields and most of their behaviour.
  • A framework such as Django expects you to extend one of its classes.
  • Subclasses extend the superclass without breaking its promises.

That said, once an inheritance chain passes three or four levels, the code gets hard to follow. You end up opening five files just to find where a method comes from. For that reason I recommend shallow hierarchies to every team I work with.

How does inheritance work in a code example?

Take the user system of a website. Every user has a name and an email address, and an admin also needs extra rights. An admin also has a list of permissions:

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

    def introduce(self):
        return f"{self.name} ({self.email})"

    def can(self, action):
        return False

class Admin(User):
    def __init__(self, name, email, permissions):
        super().__init__(name, email)
        self.permissions = set(permissions)

    def can(self, action):
        return action in self.permissions

a = Admin("Mark", "mark@example.com", ["delete", "edit"])
print(a.introduce())      # inherited method
print(a.can("delete"))    # True

Admin uses introduce without writing it, because it inherits the method from the superclass. The super().__init__ call runs the parent constructor, so you avoid repeating the name and email assignments. You also redefine the can method. We call this overriding, and it opens the door to the next principle.

A strong password policy naturally belongs in classes like these. For test accounts, you can create random values with my password generator.

When is composition a better choice than inheritance?

Composition means an object holds other objects as fields and delegates part of the work to them. It models a "has a" relationship instead of "is a". The design patterns literature repeats the same advice again and again: favour composition over inheritance where you can.

class Engine:
    def start(self):
        return "Engine started"

class Car:
    def __init__(self, engine):
        self.engine = engine

    def drive(self):
        return self.engine.start() + ", car is moving"

car = Car(Engine())

With this structure, you can write an electric engine class tomorrow and hand it to the car. Also, not a single line of Car changes. To reach the same flexibility with inheritance, you would need a separate car subclass for every engine type.

My practical rule is to say the relationship out loud. If "an admin is a user" sounds right, I use inheritance. If "an invoice is a PDF" sounds odd, I give the invoice a PDF builder object instead. This small habit saves you from tearing down fragile hierarchies later.

What is polymorphism in object oriented programming?

Polymorphism means that objects of different classes can answer the same method call in their own way. The word comes from Greek and means "many forms". Therefore the calling code does not need the exact type. It only needs to know that the expected method exists.

Here is an everyday example. Tell a guitar, a piano and a drum to "play" and all three make a sound, each in a different way. The person giving the command does not know how each instrument works inside. Software, likewise, hits the same situation all the time with payments, notifications or exports.

Polymorphism shows up in two common forms. First, there is runtime polymorphism, where subclasses override a superclass method. Second, languages like Java and C# let you define methods with the same name but different parameters, which we call overloading. Python does not support overloading directly, so you use default parameters instead.

The real gain is that adding a new type leaves existing code untouched. As a result, long if and elif chains give way to small, independent classes.

How do you apply polymorphism in code?

Take the checkout step of an online shop. Card payment, bank transfer and cash on delivery do different jobs. Still, the order code wants to talk to all of them the same way:

class CardPayment:
    def pay(self, amount):
        return f"Charged {amount} to the card"

class BankTransfer:
    def pay(self, amount):
        return f"Sent IBAN details for {amount}"

class CashOnDelivery:
    def pay(self, amount):
        return f"Will collect {amount} on delivery"

def complete_order(method, amount):
    print(method.pay(amount))

for method in [CardPayment(), BankTransfer(), CashOnDelivery()]:
    complete_order(method, 75)

The complete_order function never asks which payment type it has. Instead, it calls pay and each object gives its own answer. If you add a wallet payment next month, you write one new class and leave the order code alone.

Checkout design depends on user experience as much as on architecture. On my ecommerce consulting page I explain what I look for in the payment step.

Is duck typing a form of polymorphism?

Yes, most polymorphism in Python works through duck typing. In the previous example, the three payment classes shared no parent class. Even so, the same function handled all of them. Python did not check the type; it only checked whether the expected method existed.

The Python glossary definition sums it up. Rather than inspecting an object's type, you simply call its method or use its attribute. The name comes from an old saying: if it walks like a duck and quacks like a duck, treat it as a duck.

This flexibility is powerful, but it carries a risk. If one class spells the method payment instead of pay, the error only appears when that line runs. On large projects I therefore use one of two safeguards:

  1. Write the expected interface with type hints and typing.Protocol, then check it with a tool such as mypy.
  2. Define a shared abstract class and force subclasses to implement the method.

The second option leads straight to the fourth principle, abstraction.

What is abstraction and how does it differ from encapsulation?

Abstraction means putting what an object does in front and pushing how it does it into the background. You show users only the interface they need. When you drive, you use the wheel, the pedals and the brake. You never need the timing of the fuel injection, because the car hides it.XX of the fuel injection.

Many developers mix up the two ideas, since both seem to hide detail. Their focus differs, though. Abstraction is a design decision that answers "which abilities should this object offer?" Encapsulation, in contrast, is an implementation technique that answers "how do I protect the internal state?"

QuestionAbstractionEncapsulation
What does it focus on?What the object doesHow the data stays safe
When do you think about it?DesignImplementation
Python toolabc module, ProtocolUnderscore convention, property
GainHides complexityBlocks invalid state

Put simply, abstraction asks "what will I show?" and encapsulation asks "what will I protect?" A good class does both.

How do you write an abstract class with a code example?

In Python you write abstract classes with the abc module from the standard library. According to the abc module documentation, you cannot create an instance of a class that still has abstract methods it has not overridden. That rule forces subclasses to honour the contract:

from abc import ABC, abstractmethod

class Notification(ABC):
    def __init__(self, recipient):
        self.recipient = recipient

    @abstractmethod
    def send(self, message):
        ...

    def send_and_log(self, message):
        result = self.send(message)
        print(f"Log: {self.recipient} / {result}")

class EmailNotification(Notification):
    def send(self, message):
        return f"Email sent: {message}"

class SmsNotification(Notification):
    def send(self, message):
        return f"SMS sent: {message[:160]}"

If you try to create Notification() directly, Python raises a TypeError. That happens because send is still abstract. Meanwhile, send_and_log is a concrete method, and every subclass gets it for free.

To test length limits such as the 160 character SMS cap, my word counter comes in handy. Measure your message templates there first, then move them into code.

How do the four principles work together in one project?

Learning each principle alone is easy; the real skill is combining them. For example, look again at the notification system. All four principles play a role in that small structure:

  • Abstraction: the Notification class describes only the ability to send and hides channel details.
  • Inheritance: the email and SMS classes inherit the recipient field and the logging method.
  • Polymorphism: the same send_and_log call gives a different result on each channel.
  • Encapsulation: each channel keeps its connection details inside and never exposes them.
channels = [EmailNotification("alice@example.com"),
            SmsNotification("+15550000000")]
for channel in channels:
    channel.send_and_log("Your order has shipped")

This loop keeps working when you add a WhatsApp channel. You only write a new class that extends Notification. That way you grow the system without touching code you already tested. I describe the same thinking applied to the front end in my article on micro frontends.

What are the most common mistakes in object oriented programming?

In projects I have inherited over the years, the same mistakes come up again and again. Most of them do not come from missing knowledge. Instead, they come from applying the principles too much or in the wrong place:

  • God class: one class that knows everything and grows to thousands of lines.
  • Deep inheritance: hierarchies with five or six levels that make every lookup painful.
  • Pointless getters and setters: unchecked accessors on every field, which is ceremony rather than encapsulation.
  • Type check chains: long isinstance blocks where polymorphism would do.
  • Premature abstraction: three layers of interfaces for a single use case.

What these mistakes share is that the code resists change. Moreover, poor structure often shows up in performance too. For instance, needless object creation and repeated queries slow a page down. To measure your site speed, see my guide on the Google Lighthouse performance test.

Are OOP and functional programming rivals?

No, most modern languages support both paradigms side by side. Functional programming focuses on immutable data and functions without side effects. Object oriented programming, in contrast, gathers state inside objects. Each, therefore, gives you strong tools for different problems.

For instance, using map, filter or list comprehensions inside a Python class feels completely natural. On the JavaScript and TypeScript side, React components moved from classes to functions and hooks over time. Even so, business rules, data models and service layers still live in classes in most projects.

My approach is pragmatic. In practice, I think functionally for data transformations and in objects for domain models. What matters is the next person who reads the code, not loyalty to one paradigm.

There is also a neat parallel on the web. The JSON-LD markup that describes a page to search engines is itself a model with types, properties and nested objects. A Product holds a Brand and an Offer. If that interests you, read my schema markup guide; class thinking makes the markup easy to read.

In what order should you learn object oriented programming?

The most common beginner mistake is memorising the four principles without writing any code. That is because these ideas only click when you try them in small projects. Based on my own experience, I suggest the order below. Treat it as a starting point, not a fixed curriculum:

  1. Get comfortable with classes, objects, constructors and self through tiny examples.
  2. Try encapsulation with a bank account or stock class.
  3. Apply inheritance with a two level hierarchy such as user and admin.
  4. Rewrite that hierarchy with composition and compare the result.
  5. Build polymorphism into a payment or notification scenario.
  6. Write an abstract class with the abc module and make subclasses follow it.
  7. Finally, move on to the SOLID principles and basic design patterns.

I also suggest writing a small test at every step. For example, proving that "a negative amount must raise an error" teaches you why encapsulation matters far better than any definition.

A small exercise: how do you write a slug generator class?

To practise, pick a familiar web task: turning a title into a URL slug. It suits encapsulation well, because the conversion rules should stay hidden from callers:

import re

class SlugGenerator:
    def __init__(self, separator="-"):
        self._separator = separator

    def make(self, title):
        text = title.lower()
        words = re.findall(r"[a-z0-9]+", text)
        return self._separator.join(words)

print(SlugGenerator().make("What Is Object Oriented Programming?"))

The output is what-is-object-oriented-programming. After that, extending it is up to you. You could write a subclass that handles accented letters, or switch the separator to an underscore. Compare your output with my slug generator tool and you will quickly spot the edge cases you missed.

The exercise also teaches a little SEO. A clean URL structure also helps search engines understand a page. So this tiny class does real work in a real web project.

What does OOP knowledge give you in a web project?

A corporate website or ecommerce platform lives for years. During that time, in addition, new payment methods, new languages and new integrations arrive. A codebase that follows object oriented principles absorbs these changes as small, independent classes. A careless structure, however, breaks something else with every change.

In my web design projects I ask the development team two questions at the first meeting. Where do the business rules live, and how many files does a new feature touch? Those two answers quickly show how faithful the code is to object oriented principles.

In the end, the four principles are not a checklist; they are habits that make change cheaper. Encapsulation protects data, inheritance cuts repetition, polymorphism makes new types easy to add, and abstraction hides complexity. The natural next step is SOLID, which I cover in a separate article in the software category.

Frequently Asked Questions

Which language is best for learning object oriented programming?
Python is the easiest place to start, because its syntax is simple and classes need very little ceremony. Java or C#, on the other hand, enforce access modifiers and interfaces at compiler level, so they teach the principles more strictly. Starting with one and then trying the other lets you see the same concepts from two useful angles.
What are the four principles of OOP?
The four principles are encapsulation, inheritance, polymorphism and abstraction. Encapsulation protects data, inheritance shares common behaviour, polymorphism lets the same call produce different results, and abstraction hides detail behind a simple interface. Used together, they make code easier to change and far less prone to bugs as a project grows over time.
Is Python a fully object oriented language?
Yes, in Python every value is an object, including numbers, strings and functions. However, Python does not force you to write classes; procedural or functional styles work too. Private attributes rely on the underscore convention rather than the compiler. So in Python, object oriented discipline depends largely on the habits and agreements of your team.
What is the difference between inheritance and an interface?
Inheritance means a class takes over the code and behaviour of another class. An interface only defines which methods must exist and usually contains no code. In Java and C#, a class can extend just one superclass but implement many interfaces. In Python, abstract classes or Protocol usually take on the role of an interface.
Can you be a good developer without knowing OOP?
You can, but most codebases you will meet at work are written with classes. To read and safely change Java, C#, PHP or Python projects, you need to understand the four principles. Even if you prefer functional programming, these concepts come up constantly in interviews and code reviews, so a solid basic grasp is essential.
What should I learn after object oriented programming?
The natural next step is the SOLID principles. These five rules show you how to apply the four core principles well and keep your classes resilient to change. After that, you can study basic design patterns such as factory, strategy and observer. Trying each topic in a small project gives far more lasting results than memorising.
#object oriented programming#OOP#encapsulation#inheritance#polymorphism#abstraction#Python
Share:
Talha Aslan
Talha Aslan

Google Partner digital marketing expert. Hands-on with SEO, Google Ads, web design and e-commerce projects since 2012; every post here comes from that experience.

Next project

Let's talk about your project.

No middlemen, no layers: you talk directly to the expert doing the work. The first consultation is free, I listen to your goal and come back with a clear roadmap.

WhatsApp Call Now