Python OOPs Concepts โ Classes and Objects Explained Properly
Everything you actually need to know about Object-Oriented Programming in Python โ classes, objects, self, __init__, the four pillars (encapsulation, inheritance, polymorphism, abstraction), and the exact TypeError you'll hit while learning this.
Last Updated
March 2026
Read Time
21 min
Level
Beginner
What is OOP (Object-Oriented Programming) in Python?
Object-Oriented Programming is a way of organising code around objects โ self-contained bundles that hold both data (attributes) and the functions that operate on that data (methods) together, instead of keeping data and logic as separate, loosely-related things floating around a script. A BankAccount object holds its own balance and comes with its own deposit() and withdraw() methods bolted directly to it โ the data and the behaviour that's allowed to touch that data live in the same place, on purpose.
In Python, you define the blueprint for these objects using the class keyword. A class is the template; an object (or instance) is a specific thing built from that template. class Dog: defines what a dog generally looks like โ it has a name, a breed, it can bark โ and my_dog = Dog("Bruno", "Labrador") creates one specific, real dog object following that template, with its own actual name and breed values, distinct from any other Dog object you create.
Here's something worth saying plainly, since a lot of tutorials skip it: Python doesn't force you to use OOP. You can write a completely functional Python script using nothing but plain functions and dictionaries, and plenty of good scripts do exactly that. OOP earns its complexity when you're modelling something with genuine identity and internal state that persists and changes over time โ a user account, a game character, a database connection โ not for every single script you write. My honestly opinionated take: I've seen more damage done by forcing unnecessary classes onto a 40-line script than by beginners avoiding OOP too long. Learn it properly, then use judgment about when it actually earns its place.
This guide covers class and object mechanics, the self keyword and why it exists, the __init__ constructor, and the four pillars traditionally used to describe OOP โ encapsulation, inheritance, polymorphism, and abstraction โ with the exact errors you'll run into along the way, including the notorious TypeError: missing 1 required positional argument: 'self'.
History โ How Python's OOP Support Developed
Python has supported classes since its very first release, but the specific way object-oriented code is written in Python has genuinely evolved, with one particularly significant unification that changed how the whole language thinks about types.
- โถ
1991 โ Python 0.9.0 โ Classes, inheritance, and exception handling as classes were present from Python's first public release โ Guido van Rossum's design notes describe classes as a core feature from the very beginning, not a later addition.
- โถ
Early-to-mid 1990s โ Old-Style Classes โ Early Python had a distinction between 'old-style' classes (which didn't inherit from a common root type) and built-in types like int or list, which were implemented differently in C and behaved with subtly different rules โ a genuine wart in the language's early design.
- โถ
2002 โ Python 2.2 (PEP 252, 253) โ Introduced 'new-style' classes, unifying user-defined classes and built-in types under a single object model, all ultimately inheriting from a common object base type. This was a foundational change that made Python's type system genuinely consistent.
- โถ
2008 โ Python 3.0 โ Made new-style classes the ONLY kind โ every class in Python 3 automatically inherits from object, whether you write class Dog(object): or simply class Dog:, finally eliminating the old-style/new-style distinction entirely.
- โถ
2015 โ Python 3.5 โ Introduced typing.NamedTuple and enhanced dataclass-adjacent patterns, offering lighter-weight alternatives to full classes for simple data-holding structures.
- โถ
2018 โ Python 3.7 (PEP 557) โ Introduced the @dataclass decorator, dramatically reducing the boilerplate needed for classes whose main job is holding structured data, auto-generating __init__, __repr__, and __eq__ methods based on class-level type annotations.
- โถ
2026 โ Current state โ Full classical OOP support โ classes, inheritance, multiple inheritance, abstract base classes via the abc module, and dataclasses for lighter-weight cases โ all coexist as options, letting Python developers choose exactly how much OOP ceremony a given problem actually needs.
Key Characteristics of Python Classes and Objects
Here are the 12 things worth knowing precisely about how Python's object model actually works:
class Dog: followed by an indented block defines a class. By convention, class names use PascalCase (Dog, BankAccount), distinguishing them visually from function and variable names, which use snake_case.
The __init__ method runs automatically whenever a new object is created from the class, typically used to set up the object's initial attribute values based on arguments passed at creation time.
Every regular method's first parameter, conventionally named self, refers to the specific object the method was called on โ it's how a method accesses and modifies that particular object's own attributes.
Two objects created from the same class are completely independent โ changing one object's attribute has zero effect on another object of the same class, unless they're explicitly sharing a mutable class-level attribute.
A class can inherit from another class, automatically gaining its attributes and methods while being free to override or extend them โ class Puppy(Dog): builds on everything Dog already defines.
Different classes can implement the same method name with different behaviour, letting code written against a common interface work correctly regardless of the specific object's exact type.
Python doesn't enforce true private attributes the way Java does โ a leading underscore (_balance) signals 'internal use, don't touch directly' by convention, while double underscore (__balance) triggers name mangling for a stronger (but still not absolute) form of privacy.
Methods like __str__, __eq__, __len__, and __add__ (all with double underscores, hence 'dunder') let a custom class integrate with Python's built-in operators and functions โ print(), ==, len(), + โ behaving naturally rather than needing special custom syntax.
Attributes defined directly inside the class body (outside any method) are shared across every instance by default. Attributes assigned via self.x = ... inside __init__ belong to that specific instance alone.
The abc module's ABC base class and @abstractmethod decorator let you define a class that cannot be instantiated directly and forces subclasses to implement specific methods, genuinely enforced at runtime.
Unlike Java, a Python class can inherit from more than one parent class simultaneously: class FlyingFish(Fish, Bird): โ resolved via a defined, predictable method resolution order (MRO).
Integers, strings, functions, and even classes themselves are objects in Python, all ultimately inheriting from the base object type โ this uniformity is why type(5) returns <class 'int'>, treating even a plain number as an instance of a class.
How an Object Gets Created and Used โ Flowchart
Creating an object and calling a method on it goes through a specific, precise sequence โ Python allocates memory, runs your constructor, and then routes every method call back to that exact object via self. Here's that full pipeline traced through.
Code Execution Flow โ from source to output
Key insight: step 7 is the part beginners most often miss โ when you write my_dog.bark(), you never actually pass my_dog as an argument yourself. Python does that automatically, behind the scenes, which is exactly why bark(self): needs that self parameter in its definition even though you never see it supplied at the call site.
How OOP Actually Works โ self, __init__, and the Error They Explain
Three things separate someone who's copied a class template from someone who actually understands Python's object model: knowing exactly what self is and why it's required, understanding what __init__ genuinely does (and doesn't do), and recognising the specific error that appears the moment either of these is misunderstood.
๐ซด self โ Not Magic, Just an Explicit Parameter
Here's the thing that makes self click for a lot of people: it's not a keyword, and it's not special syntax. It's a completely ordinary parameter name โ Python could work exactly the same if you called it this or instance instead, though breaking the self convention would confuse literally every other Python developer who reads your code. When you write my_dog.bark(), Python automatically translates that, internally, into something functionally equivalent to Dog.bark(my_dog) โ the object you called the method on gets passed as the first argument, automatically, every single time. That's the entire mechanism. self is just the name conventionally given to that first, automatically-supplied argument.
๐ฅ The Classic Error โ Forgetting self
Write a method without the self parameter โ def bark(): instead of def bark(self): โ and calling my_dog.bark() raises TypeError: bark() takes 0 positional arguments but 1 was given. This confuses nearly everyone the first time, because you genuinely didn't pass any arguments at the call site. But Python still automatically supplies my_dog as that first argument regardless of whether your method's signature has room for it โ the method just wasn't written to accept it. This is one of the most common beginner OOP errors, and understanding exactly why it happens (rather than just memorising 'always add self') is what actually prevents it from recurring in slightly different forms later.
๐ง __init__ โ Setup, Not Creation
__init__ is commonly called 'the constructor,' but that's a slight simplification worth being precise about: the object has technically already been created (allocated in memory) by the time __init__ runs โ __init__'s job is initialising that already-existing object's starting state, not creating the object itself (that's technically a separate, rarely-touched method called __new__). In practice, this distinction rarely matters day to day, but it explains why __init__ never has a return statement returning a value โ its whole job is setting attributes on self, and returning anything other than None from __init__ actually raises TypeError: __init__() should return None.
Simple rule to remember: every regular method needs self as its first parameter, Python supplies that argument automatically whenever you call a method on an object, and __init__ is where you set up an object's starting attributes using self.attribute_name = value, never returning anything from it.
The Four Pillars of OOP โ Key Differences
Encapsulation, inheritance, polymorphism, and abstraction are the four concepts traditionally used to describe what makes a language 'object-oriented.' Here's what each one actually means in concrete Python terms.
OOP Implementation โ Python vs Other Languages
Python's OOP model is genuinely more relaxed than Java's or C++'s in several specific, deliberate ways, and the contrast explains a lot about how Python code tends to look different from enterprise-style OOP code.
Here's my honestly opinionated take: Python making self an explicit, visible parameter โ rather than an implicit, invisible one like Java's this โ is one of its most underrated teaching advantages. It forces every Python developer to actually see, in the method signature itself, exactly how an object gets access to its own data, rather than treating it as invisible magic. I've watched people coming from Java genuinely struggle to explain how this works internally, while Python developers who've internalised self as 'just the object, passed as the first argument, automatically' tend to have a noticeably clearer mental model of what's actually happening.
Advantages and Disadvantages of Python's OOP Approach
Python's relaxed, convention-based approach to OOP is a genuine trade-off โ flexible and fast to write, but leaning heavily on developer discipline rather than compiler enforcement.
Python's Object Model Architecture
The diagram below shows how classes, instances, and inheritance actually relate to each other in Python's underlying object model โ from your class definition down to how attribute lookup actually resolves at runtime.
OOP in Practice โ From a Basic Class to Inheritance and the Classic Error
Here's a complete, working example covering a base class, inheritance, method overriding, and encapsulation โ followed by the exact self-related error and its fix.
class Animal:
def __init__(self, name, sound):
self.name = name
self._sound = sound # single underscore = internal convention
def speak(self):
return f"{self.name} says {self._sound}"
def __str__(self):
return f"Animal({self.name})"
class Dog(Animal): # inheritance
def __init__(self, name, breed):
super().__init__(name, sound="Woof")
self.breed = breed
def speak(self): # polymorphism โ overriding the parent method
return f"{self.name} the {self.breed} barks: Woof!"
animals = [Animal("Generic Creature", "..."), Dog("Bruno", "Labrador")]
for a in animals:
print(a.speak()) # each object responds in its own way
print(animals[1]) # uses __str__ automaticallyOutput
Generic Creature says ... Bruno the Labrador barks: Woof! Animal(Bruno)Now the classic beginner mistake โ forgetting self โ reproduced exactly:
class Dog:
def __init__(self, name):
self.name = name
def bark(): # BUG: missing self
print("Woof!")
my_dog = Dog("Bruno")
my_dog.bark()Output
Traceback (most recent call last): File "missing_self.py", line 8, in <module> my_dog.bark() TypeError: bark() takes 0 positional arguments but 1 was givenPractice This Code โ Live Editor
Line-by-Line Explanation
- โถ
class Dog(Animal):โ This is inheritance.Dogautomatically gains everythingAnimaldefines, and can add its own attributes or override methods. - โถ
super().__init__(name, sound="Woof")โ Calls the parent class's__init__to reuse its setup logic, rather than duplicatingself.name = nameagain insideDog. - โถ
def speak(self):defined again insideDogโ This is polymorphism through method overriding. Calling.speak()on anAnimalobject and aDogobject runs genuinely different code, even though the method is called identically in both cases. - โถ
def __str__(self):โ A dunder method that customises whatprint(animals[1])actually displays, letting a custom object integrate naturally with Python's built-inprint()function instead of showing a default, unhelpful memory address. - โถ
def bark():withoutselfโ The fix is simply addingselfas the first parameter:def bark(self):. Python was already automatically supplyingmy_dogas an argument the momentmy_dog.bark()was called; the method definition just wasn't written to receive it.
Where OOP Actually Earns Its Place โ Real-World Applications
OOP isn't just an academic exercise โ it's how a substantial share of real, professional Python code is genuinely structured:
- โถ
๐ Django and Web Framework Models โ Every Django database table is defined as a Python class inheriting from models.Model, with fields as class attributes and custom business logic as methods โ genuinely OOP-driven, end to end.
- โถ
๐ค Machine Learning Model Classes โ PyTorch's nn.Module and scikit-learn's estimator classes are built entirely around inheritance and method overriding โ a custom neural network is a class inheriting from nn.Module, overriding its forward() method.
- โถ
๐ฎ Game Development โ Game entities (Player, Enemy, Projectile) are classic OOP use cases, typically sharing a common base class for shared behaviour (position, health) while each subclass overrides methods for its specific behaviour.
- โถ
๐ฆ Business Domain Modelling โ Real-world business entities โ Customer, Order, Invoice, BankAccount โ map naturally onto classes, with encapsulation keeping validation logic (like preventing a negative balance) tightly coupled to the data it protects.
- โถ
๐งช Custom Exception Hierarchies โ Well-designed applications define their own exception classes inheriting from Exception, letting calling code catch specific, meaningful error types (InsufficientFundsError) rather than generic ones.
- โถ
๐ Building Reusable Libraries and APIs โ Library authors use abstract base classes (via the abc module) to define a required interface, forcing anyone extending the library to implement specific methods, genuinely enforced at runtime.
- โถ
๐ GUI Application Development โ Tkinter, PyQt, and similar GUI frameworks are built around widget classes, and application-specific windows and dialogs are typically written as classes inheriting from the framework's base widget classes.
- โถ
โ๏ธ API Client Wrappers โ Wrapping a third-party API often means defining a class that holds connection state (API key, session) as instance attributes, with methods for each API endpoint โ a natural fit for encapsulation.
Why OOP Is Worth the Real Effort It Takes
OOP has a genuine learning curve, and it's fair to be honest about that rather than pretending classes are just 'functions with extra steps.' Here's why the investment pays off:
- โถ
๐๏ธ It's How Most Real Frameworks Are Structured โ Django, PyTorch, Tkinter, and most substantial Python libraries expect you to write classes that inherit from their base classes. Not understanding OOP genuinely blocks effective use of these tools.
- โถ
๐ผ It's a Core Interview Topic at Every Level โ Explaining the four pillars, designing a small class hierarchy on a whiteboard, or predicting the output of an inheritance example are all standard, recurring interview scenarios, from fresher to senior roles.
- โถ
๐ง It Teaches a Genuinely Different Way to Model Problems โ Thinking in terms of 'what are the things in this system, and what do they know and do' is a distinct, valuable design skill separate from procedural, top-to-bottom scripting.
- โถ
๐ง It's the Foundation for Design Patterns โ Nearly every classic software design pattern (Factory, Observer, Strategy, Singleton) is expressed in terms of classes, inheritance, and polymorphism โ OOP fluency is a genuine prerequisite for understanding them.
- โถ
๐ Understanding self Prevents an Entire Class of Bugs โ The forgotten-self TypeError, along with confusion about instance versus class attributes, accounts for a real share of early OOP debugging time โ properly understanding the mechanism eliminates both almost entirely.
- โถ
๐ฎ๐ณ It's a Major, Heavily Weighted Topic in Indian CS Curricula โ OOP concepts, often taught first in C++ or Java before Python, are consistently one of the most heavily tested topics in Indian engineering college exams and campus placement technical rounds.
A Closer Look โ Encapsulation and Inheritance Patterns
Two of the four pillars deserve a bit more concrete detail, since they show up constantly in real class design decisions.
- โถ
Public attributes (no underscore) โ self.name โ freely accessible and modifiable from outside the class, no restriction implied at all. The default, most common case.
- โถ
Protected attributes (single underscore) โ self._balance โ a convention signalling 'this is internal, treat it as implementation detail, don't touch it directly from outside the class,' but Python enforces absolutely nothing here; it's purely a courtesy signal to other developers.
- โถ
Private attributes (double underscore) โ self.__balance โ triggers Python's name mangling, internally renaming the attribute to _ClassName__balance. This makes accidental external access noticeably harder (you'd need to know the mangled name), though it's still technically reachable, not truly private in the Java sense.
- โถ
@property for controlled access โ Using the @property decorator lets you expose what looks like a plain attribute externally (account.balance) while actually running validation logic behind the scenes internally โ the cleanest, most Pythonic form of real encapsulation.
- โถ
Single inheritance โ class SavingsAccount(BankAccount): โ the standard, most common inheritance pattern, extending one parent class.
- โถ
Multiple inheritance โ class FlyingFish(Fish, Bird): โ inheriting from more than one class at once, resolved via Python's Method Resolution Order (MRO), calculated using the C3 linearization algorithm, viewable via ClassName.__mro__.
- โถ
super() for cooperative inheritance โ Calling super().__init__(...) lets a subclass extend its parent's setup logic rather than duplicating or completely replacing it, and correctly participates in more complex multiple-inheritance chains too.
Python OOPs Concepts โ Interview Questions
These are among the most consistently asked Python interview questions across every experience level, from campus placements to senior backend interviews.
Practice Questions โ Test Your Understanding
Work through these before checking the answers. Correctly tracing which class's method actually runs โ especially with inheritance and overriding โ is the real test here.
1. What is the output of: class Dog: def __init__(self, name): self.name = name \n d = Dog('Bruno') \n print(d.name)
Easy2. What error occurs, and why, if you run: class Cat: def meow(): print('Meow') \n c = Cat() \n c.meow()?
Easy3. Trace this: class Animal: def speak(self): return 'generic sound' \n class Dog(Animal): def speak(self): return 'Woof' \n a = Animal() \n d = Dog() \n print(a.speak(), d.speak())
Medium4. What is the output of: class Counter: count = 0 \n def __init__(self): Counter.count += 1 \n a = Counter() \n b = Counter() \n c = Counter() \n print(Counter.count)
Medium5. Why does this raise an error? class BankAccount: def __init__(self, balance): self.__balance = balance \n acc = BankAccount(100) \n print(acc.__balance)
Hard6. What does class Shape(ABC): @abstractmethod def area(self): pass followed by shape = Shape() produce?
Medium7. What is the output of: class A: def greet(self): return 'A' \n class B: def greet(self): return 'B' \n class C(A, B): pass \n print(C().greet())
Hard8. What does super().__init__(name, sound='Woof') accomplish inside a subclass's own __init__ method?
MediumConclusion โ Objects Are a Modelling Tool, Not a Requirement
OOP is genuinely one of the more conceptually dense topics in learning Python, precisely because it isn't just new syntax โ it's a different way of thinking about how to structure a program's data and behaviour together. self, __init__, inheritance, and the four pillars all click eventually, usually not from reading a definition but from building a few real classes and watching, first-hand, how an object's state persists and changes across multiple method calls.
If you're a complete beginner, the priority is getting comfortable with the basic mechanics โ class, __init__, self, and calling methods on instances โ until writing a simple class feels automatic. If you're building real, larger applications, the priority shifts to judgment: recognising when a problem's structure genuinely calls for a class hierarchy versus when a handful of plain functions would be clearer, faster to write, and easier for someone else to follow six months later.
The habit worth carrying forward: before reaching for a class, ask honestly whether the problem actually has meaningful, persistent state that needs protecting and behaviour that genuinely belongs bundled with that state โ not just because OOP is what you're 'supposed' to use for anything beyond a script. Classes used deliberately, for the problems that genuinely call for them, are one of the most powerful organising tools in the language. Classes used out of habit, for everything, just add ceremony.
OOP in Python is genuinely flexible by design โ mostly convention, lightly enforced. Learn the real mechanics behind self and __init__ properly, understand what each of the four pillars actually buys you, and you'll have real judgment about when to reach for a class and when to leave it as a plain function. ๐