Object-Oriented Programming
Object-Oriented Programming (OOP) is a way to write code by grouping related data and actions together inside something called an “object”. Instead of writing lots of separate functions and variables floating around, you create small models that represent real things - like a bank account, a cup of tea, or a user. This makes your code cleaner, easier to understand, and easier to grow over time, especially in bigger projects like apps and libraries.
Main Ideas
Section titled “Main Ideas”| Concept | What it means |
|---|---|
| Class | A blueprint or template to create objects |
| Object | A real thing created from a class |
| Attribute | A piece of data stored inside an object or class |
| Method | A function that lives inside a class |
| Encapsulation | Keeping data and actions together in one place |
| Inheritance | One class borrowing from another class |
| Polymorphism | Different classes using the same method name but doing different things |
| Abstraction | Hiding the complex stuff and showing only what matters |
Each of these ideas is about modeling “things” instead of writing scattered, unorganized code.
Classes and Objects
Section titled “Classes and Objects”A class is like a template or a cookie cutter. An object is the actual cookie made from that cutter. When you create an object, Python gives it its own space in memory to store its own data, separate from other objects.
class BankAccount: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
def deposit(self, amount): self.balance += amount
def summary(self): return f"{self.owner}: {self.balance}"
account = BankAccount("Asha", 1000)account.deposit(250)print(account.summary())Explanation:
BankAccountis the blueprint.accountis a real object with its own data.- Methods operate on that object’s data.
Namespace and Attribute Lookup
Section titled “Namespace and Attribute Lookup”A namespace is just a list that maps names to values - like a dictionary. In OOP, there are two main namespaces you care about: one for the object (instance) and one for the class. When you look up a name, Python checks them in a specific order.
class Tea: origin = "India"
masala = Tea()masala.origin = "Nepal"Explanation:
Tea.origin-> lives in the class namespacemasala.origin-> lives in the object’s own namespace (after you set it)- The object’s value wins over the class value
Lookup Order:
Python first checks the object, then the class, then Python’s built-in stuff.
Attribute Shadowing
Section titled “Attribute Shadowing”Attribute shadowing is when an object creates its own variable with the same name as a class variable. The object’s version covers up the class version - but the class version is still there, untouched.
class Tea: temperature = "hot"
cup = Tea()print(cup.temperature) # hot
cup.temperature = "warm"print(cup.temperature) # warmprint(Tea.temperature) # hot
del cup.temperatureprint(cup.temperature) # hot againExplanation:
- The object’s attribute takes priority when you look it up
- If you delete the object’s attribute, Python falls back to the class attribute
self is a way for each object to refer to itself. It lets each object store its own separate data. Without self, all objects of the same class would share the same variables, which would cause a lot of bugs.
class Cup: def __init__(self, size): self.size = size
def describe(self): return f"A {self.size}ml cup"Explanation:
self.sizebelongs to that specific object, not all cups- When you call
cup.describe(), Python automatically passes the object in asself
Constructor
Section titled “Constructor”__init__ is a special method that runs automatically when you create a new object. It sets up the object’s starting state - kind of like filling out a form when you sign up for something.
class BankAccount: def __init__(self, owner, balance=0): self.owner = owner self.balance = balanceExplanation:
- It gives the object its starting values
- Every object begins with a known, defined state
- You don’t have to use it, but most classes do
Inheritance and Composition
Section titled “Inheritance and Composition”Inheritance
Section titled “Inheritance”Inheritance lets one class (the child) copy and extend another class (the parent). This way you don’t have to rewrite the same code twice.
class Account: def describe(self): return "Base account"
class SavingsAccount(Account): def withdraw(self, amount): return f"Withdrawing {amount}"Explanation:
SavingsAccountautomatically gets thedescribe()method fromAccount- You can add new features without rewriting the old ones
Composition
Section titled “Composition”Composition means building a class by putting other objects inside it, instead of inheriting from them. It’s like building a car by plugging in an engine, rather than making a “CarEngine” that inherits from both.
class StatementPrinter: def print_summary(self, account): return account.describe()
class Bank: def __init__(self): self.printer = StatementPrinter()Explanation:
BankusesStatementPrinteras a tool- This is more flexible than inheritance
- You can easily swap one component for another
Calling the Base Class
Section titled “Calling the Base Class”When a child class has its own __init__, it needs to call the parent’s __init__ too, or the parent’s setup gets skipped. You do that with super().
class BaseAccount: def __init__(self, owner, balance): self.owner = owner self.balance = balance
class SuperAccount(BaseAccount): def __init__(self, owner, balance): super().__init__(owner, balance)Explanation:
super()calls the parent class’s method- This is important when you have a chain of classes inheriting from each other
- It avoids repeating the same setup code
MRO (Method Resolution Order)
Section titled “MRO (Method Resolution Order)”MRO is the order Python follows when looking for a method, especially when a class inherits from multiple parents. Python figures this out automatically using a set of rules called C3 linearization.
class A: passclass B(A): passclass C(A): passclass D(C, B): pass
print(D.__mro__)Output:
D -> C -> B -> A -> objectExplanation:
- Python uses C3 linearization to figure this out
- This makes sure there’s always one clear, predictable order to search in
Static Methods and Class Methods
Section titled “Static Methods and Class Methods”Static Method
Section titled “Static Method”A static method is just a regular function that you put inside a class because it logically belongs there. It doesn’t need access to the object or the class.
class TextTools: @staticmethod def clean(text): return [item.strip() for item in text.split(",")]Explanation:
- No
selforclsneeded - Used for helper or utility functions that are related to the class
Class Method
Section titled “Class Method”A class method gets the class itself passed in (as cls), instead of the object. This is useful when you want to create objects in different ways.
class BankAccount: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
@classmethod def from_string(cls, text): owner, balance = text.split("-") return cls(owner, int(balance))Explanation:
- Used to give users an alternative way to create objects
- Very useful when building libraries
Why class methods are important in libraries
Section titled “Why class methods are important in libraries”When building libraries:
- You don’t want users to always call
__init__manually - You provide clean, easy ways to create objects
Example:
user = User.from_json(data)user = User.from_db(row)This improves:
- readability
- flexibility
- maintainability
Private Attributes and Properties
Section titled “Private Attributes and Properties”Python doesn’t block you from accessing any variable, but it uses naming rules to signal what should stay private.
_name-> meant for internal use only (soft warning)__name-> Python changes the name internally to make it harder to access by accident (called name mangling)
Properties let you control how someone reads or writes a value.
class Account: def __init__(self, name): self._name = name self._balance = 0
@property def balance(self): return self._balance
@balance.setter def balance(self, amount): if amount >= 0: self._balance = amountExplanation:
- You access
balancelike a normal variable, but behind the scenes, logic runs - This lets you add checks like “balance can’t be negative”
Overriding and Overloading
Section titled “Overriding and Overloading”Overriding
Section titled “Overriding”Overriding is when a child class replaces a method it got from the parent with its own version.
class A: def message(self): return "A message"
class B(A): def message(self): return "B message"Explanation:
- Both classes have a method called
message - But each one does something different
Overloading
Section titled “Overloading”Python doesn’t support true overloading (having multiple functions with the same name but different inputs). Instead, you handle it with default values.
def add(a, b=0, c=0): return a + b + cExplanation:
- One function handles multiple cases depending on how many arguments you pass
Deprecating Methods
Section titled “Deprecating Methods”from warnings import deprecated
class API: @deprecated("Use new_function() instead") def old_method(self): # this warning is optional, but helps users know about deprecation and encourages them to switch to the new method warnings.warn( "old_method is deprecated, use new_method instead", DeprecationWarning, stacklevel=2 )
def new_method(self): return "new"Explanation:
- Shows a warning when someone uses the old method, without breaking their code
- Very common in frameworks and libraries when updating to new versions
- Gives users time to switch over before you remove the old method
Best practice:
- Keep the old method around for a while
- Add a warning message
- Remove it later once everyone has moved on
Destructor
Section titled “Destructor”__del__ is a special method that Python calls just before an object gets deleted from memory. Think of it as a goodbye function.
class Test: def __del__(self): print("Object destroyed")Explanation:
- You can’t rely on this for important cleanup because you don’t control exactly when Python deletes objects
- Python’s garbage collector decides the timing, which can be unpredictable