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…

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 theyprint()the object__repr__— what developers see in a debugger or the console; should be unambiguous__len__— makeslen(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