← Back to list

Python OOP for Beginners: Write Code That Actually Scales

You’ve got the basics down. Variables, loops, functions — you can write a script that works. But the second a project grows beyond a single…

Dipanshu Sengar · 2026-07-09 15:03 · 0 claps · 5.6 min read
#python #programming #software-development #objectorientedprogramming #oops-concepts
Open on Medium ↗
Wiki topics: 💻 · Programming

Python OOP for Beginners: Write Code That Actually Scales

You’ve got the basics down. Variables, loops, functions — you can write a script that works. But the second a project grows beyond a single file, things start getting messy. You’ve got variables everywhere, functions that do five different things, and no clear way to keep it all organized.

That’s exactly where Object-Oriented Programming comes in.

Learning OOP in Python is a lot like learning to build with LEGO instead of clay. With clay, you shape everything from scratch every single time. With LEGO, you design reusable blocks once — and then snap them together however you need. The blocks don’t change. The combinations are endless.

This guide is your introduction to thinking in blocks. Not a theoretical lecture on what OOP is, but a practical walkthrough of how to use it — classes, inheritance, dunder methods, and all — with real examples you can run and build on today.

By the end, you won’t just understand OOP. You’ll be writing code that’s cleaner, reusable, and ready to grow.

1. Classes and Objects — Your Blueprint and Your Building

Everything in OOP starts with a class. Think of a class as a blueprint — it describes what something looks like and what it can do. An object is what you actually build from that blueprint.

class Employee:
    def __init__(self, name, role, salary):
        self.name = name
        self.role = role
        self.salary = salary
    def introduce(self):
        return f"Hi, I'm {self.name} and I work as a {self.role}."
# Creating objects from the blueprint
emp1 = Employee("Jordan", "Analyst", 60000)
emp2 = Employee("Sam", "Engineer", 80000)
print(emp1.introduce())  # Hi, I'm Jordan and I work as an Analyst.
print(emp2.introduce())  # Hi, I'm Sam and I work as an Engineer.

The __init__ method is the constructor — it runs automatically the moment you create a new object. The self parameter is just how Python refers to the object itself. Every method in a class gets self as its first argument; it's Python's way of saying "this particular object."

One blueprint, infinite objects. That’s the core idea.

2. Instance vs Class vs Static Methods — Three Flavors of Functions

Not every method in a class behaves the same way. Python gives you three types, and each has a clear job.

Instance methods — the most common. They operate on a specific object and always take self:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    def get_details(self):
        return f"{self.name} earns ${self.salary}"

Class methods — operate on the class itself, not on any particular object. Use @classmethod and cls instead of self. Great for alternative constructors:

class Employee:
    company = "TechCorp"
    @classmethod
    def get_company(cls):
        return cls.company
print(Employee.get_company())  # TechCorp

Static methods — don’t need access to the class or the object at all. Just a regular utility function that lives inside the class for organizational purposes:

class Employee:
    @staticmethod
    def is_valid_salary(salary):
        return salary > 0
print(Employee.is_valid_salary(50000))  # True

A simple way to remember the difference: instance methods need the object, class methods need the class, static methods need neither.

3. Inheritance and Method Overriding — Build On What Already Exists

Inheritance lets one class borrow everything from another — and then add or change what it needs to. This is the LEGO analogy in action: take an existing block, snap something new onto it.

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    def describe(self):
        return f"{self.name} is an employee earning ${self.salary}"
class Manager(Employee):
    def __init__(self, name, salary, team_size):
        super().__init__(name, salary)
        self.team_size = team_size
    def describe(self):
        return f"{self.name} manages a team of {self.team_size}"
emp = Employee("Jordan", 60000)
mgr = Manager("Sam", 90000, 8)
print(emp.describe())  # Jordan is an employee earning $60000
print(mgr.describe())  # Sam manages a team of 8

super() calls the parent class's __init__ so you don't have to rewrite it. Method overriding is when a child class redefines a method from the parent — like describe() above. The child's version takes over, but the parent's original still exists if you need it.

4. Encapsulation — Protecting Your Data

Encapsulation is the practice of controlling access to an object’s data. Not everything inside a class should be freely readable or writable from the outside.

Python uses naming conventions to signal intent:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner          # public — anyone can access
        self._balance = balance     # protected — internal use, handle with care
        self.__pin = 1234           # private — strongly restricted
    def get_balance(self):
        return self._balance
    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
account = BankAccount("Jordan", 5000)
print(account.get_balance())   # 5000 ✅
print(account.__pin)           # AttributeError ❌

One underscore (_balance) means "treat this as internal — don't touch it from outside unless you know what you're doing." Two underscores (__pin) triggers name mangling — Python deliberately makes it harder to access from outside the class. It's not a true lock, but it's a strong signal that this data is off-limits.

The takeaway: expose only what needs to be exposed. Everything else stays inside.

5. Dunder Methods — Making Your Objects Feel Native

Dunder methods (short for “double underscore”) let your custom objects behave like built-in Python types. You’ve already seen __init__ — these are its siblings.

class Task:
    def __init__(self, title, priority):
        self.title = title
        self.priority = priority
    def __str__(self):
        return f"Task: {self.title} (Priority: {self.priority})"
    def __repr__(self):
        return f"Task(title='{self.title}', priority={self.priority})"
    def __len__(self):
        return len(self.title)
    def __eq__(self, other):
        return self.title == other.title and self.priority == other.priority
t1 = Task("Write report", 1)
t2 = Task("Write report", 1)
print(str(t1))       # Task: Write report (Priority: 1)
print(repr(t1))      # Task(title='Write report', priority=1)
print(len(t1))       # 12
print(t1 == t2)      # True

Quick breakdown of what each does:

  • __str__ — what the user sees when they print() the object
  • __repr__ — what developers see in a debugger or the console; should be unambiguous
  • __len__ — makes len(your_object) work
  • __eq__ — defines what "equal" means for two objects

Once you start using dunders, your custom classes stop feeling like alien objects and start behaving like natural Python.

6. Abstract Classes and Interfaces — Enforcing a Contract

Sometimes you want to define a blueprint that forces every subclass to implement certain methods. That’s what abstract classes are for.

from abc import ABC, abstractmethod
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
    @abstractmethod
    def perimeter(self):
        pass
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def area(self):
        return self.width * self.height
    def perimeter(self):
        return 2 * (self.width + self.height)
rect = Rectangle(5, 3)
print(rect.area())       # 15
print(rect.perimeter())  # 16

Try to instantiate Shape directly and Python will throw an error — it's intentionally incomplete. Every class that inherits from it must implement area() and perimeter(), or it'll fail too.

This is Python’s version of an interface: a contract that guarantees every subclass will have certain capabilities. Especially useful in larger codebases where different developers are implementing different parts.

7. Dataclasses — OOP With Half the Boilerplate

If you’ve written a few classes and thought “I keep writing the same __init__ over and over," dataclasses are your answer. The @dataclass decorator automatically generates __init__, __repr__, and __eq__ for you.

from dataclasses import dataclass, field
@dataclass
class Employee:
    name: str
    role: str
    salary: float
    skills: list = field(default_factory=list)
emp = Employee("Jordan", "Analyst", 60000)
emp.skills.append("Python")
print(emp)
# Employee(name='Jordan', role='Analyst', salary=60000, skills=['Python'])

That’s it. No __init__, no __repr__, no self.name = name repeated for every attribute. Python handles it all.

You can also make a dataclass immutable (frozen) — great for config objects or anything that shouldn’t change after creation:

@dataclass(frozen=True)
class Config:
    host: str
    port: int
config = Config("localhost", 8080)
config.port = 9090  # Raises FrozenInstanceError ❌

Dataclasses aren’t a replacement for regular classes — if you need heavy custom logic in __init__ or complex method behavior, a regular class is still the right tool. But for clean, data-heavy objects? Dataclasses are a significant quality-of-life upgrade.

You Don’t Build Skyscrapers With Clay

Here’s what OOP actually gives you: structure. The ability to model real-world things — employees, tasks, bank accounts, shapes — as self-contained objects that carry their own data and behavior.

When your codebase is small, it feels optional. Once it grows, it feels essential.

The concepts in this guide — classes, inheritance, encapsulation, dunders, dataclasses — aren’t things you need to master all at once. Start with one. Build a small class for something real: a to-do item, an invoice, a user profile. See how it feels to snap your own LEGO blocks together.

You’ve got the blueprint. Now go build something.


메타데이터
post_id
f0d0323d20d8
slug
youve-got-the-basics-down-f0d0323d20d8
url
https://medium.com/@dipanshusengar682/youve-got-the-basics-down-f0d0323d20d8
canonical_url
https://medium.com/@dipanshusengar682/youve-got-the-basics-down-f0d0323d20d8
author_url
https://medium.com/@dipanshusengar682
status
ok
fetched_at
2026-07-29 11:52:52