๐Ÿ›๏ธ Python

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:

๐Ÿ—๏ธ
Defined with class

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.

๐Ÿ”ง
__init__ Is the Constructor

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.

๐Ÿซด
self Refers to the Specific Instance

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.

๐Ÿ“ฆ
Objects Hold Their Own State

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.

๐Ÿงฌ
Supports Inheritance

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.

๐ŸŽญ
Supports Polymorphism

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.

๐Ÿ”’
Encapsulation via Naming Convention

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.

๐ŸŽฉ
Dunder Methods Customise Behaviour

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.

๐Ÿ›๏ธ
Class Attributes vs Instance Attributes

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.

๐Ÿงช
abc Module Enables True Abstraction

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.

๐Ÿ”—
Multiple Inheritance Is Supported

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).

๐ŸŽฏ
Everything in Python Is Actually an Object

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.

๐Ÿ“ Dog("Bruno", "Labrador") CalledClass used like a function
class called
๐Ÿงฑ New Object AllocatedEmpty instance created in memory
constructor invoked
๐Ÿ”ง __init__ Runs Automaticallyself, "Bruno", "Labrador" bound
runs body
๐Ÿ“ฅ Attributes Set on selfself.name = "Bruno", etc.
construction complete
๐Ÿ“ค Fully Formed Object ReturnedAssigned to my_dog variable
later in code
๐Ÿ“ž my_dog.bark() CalledMethod called on the object
Python auto-binds
๐Ÿซด self Bound to my_dogPython passes the object automatically
runs using that instance's data
โ–ถ๏ธ Method Body ExecutesUsing self.name, self.breed, etc.

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.

PillarCore IdeaPython MechanismExample
EncapsulationBundle data and the methods that operate on it together, controlling accessUnderscore naming conventions (_x, __x), propertiesclass BankAccount: self._balance โ€” internal, accessed via methods
InheritanceA class reuses and extends another class's attributes and behaviourclass Child(Parent): syntaxclass SavingsAccount(BankAccount): adds interest logic on top
PolymorphismDifferent classes respond to the same method call in their own wayMethod overriding, duck typingBoth Dog and Cat implement speak(), called identically
AbstractionHide internal complexity behind a simple, well-defined interfaceabc module, ABC base class, @abstractmethodclass Shape(ABC): forces every subclass to implement area()

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.

FeaturePythonJavaC++JavaScript
True private attributesโŒ Convention only (_x, name mangling for __x)โœ… Enforced with private keywordโœ… Enforced with private keywordโœ… Enforced with # (ES2022+) or closures
Multiple inheritanceโœ… Fully supported, with defined MROโŒ Not for classes (interfaces only)โœ… Supported, genuinely complexโŒ Not natively (mixins simulate it)
Abstract classesโœ… Via abc module, opt-inโœ… Native abstract keywordโœ… Via pure virtual functionsโŒ Not native, simulated via convention
Method overloadingโŒ Not supported nativelyโœ… Supportedโœ… SupportedโŒ Not supported natively
Constructor overloadingโŒ Not directly (use default args instead)โœ… Supportedโœ… SupportedโŒ Not directly
Access modifier enforcementConvention-based, not compiler-enforcedCompiler-enforcedCompiler-enforcedRuntime-enforced for # fields
'this'/'self' explicitnessExplicit first parameter, always visibleImplicit this, invisible in method signatureImplicit this, invisible in method signatureImplicit this, context-dependent behaviour

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.

โœ… Advantages
Minimal Boilerplate Compared to Java/C++No mandatory access modifier keywords on every attribute, no separate header/implementation files โ€” a working Python class is often a fraction of the lines an equivalent Java class needs.
Multiple Inheritance Without Interface WorkaroundsPython lets a class genuinely inherit from several parent classes directly, unlike Java's single-inheritance-plus-interfaces model, with a clear, predictable method resolution order handling any conflicts.
Explicit self Makes the Object Model TransparentSeeing self as a real, visible parameter in every method signature demystifies exactly how instance methods access their own object's data, rather than hiding it as invisible language magic.
Duck Typing Enables Flexible PolymorphismPython doesn't require classes to share a common explicit interface to be used interchangeably โ€” if an object has the right methods, it works, regardless of formal inheritance relationships.
Dunder Methods Integrate Naturally with Built-insCustom classes can support +, ==, len(), and print() through dunder methods, making user-defined types feel like genuine first-class citizens alongside Python's built-in types.
Opt-In Complexity via dataclasses and abcSimple data-holding classes can use @dataclass to skip boilerplate entirely, while genuinely complex hierarchies can opt into strict enforcement via the abc module โ€” Python doesn't force one level of ceremony on every class.
โŒ Disadvantages
No True Access Control EnforcementPrivate attributes are purely a naming convention (_x) or a mild name-mangling deterrent (__x) โ€” nothing in Python actually PREVENTS external code from directly touching an attribute meant to be internal.
self Must Be Manually Included Every TimeForgetting self as the first parameter in every method definition is a genuinely common, recurring beginner mistake, producing a confusing TypeError the first several times it happens.
No Native Method or Constructor OverloadingUnlike Java or C++, you can't define multiple versions of __init__ or a method with different parameter types โ€” Python developers work around this with default arguments and manual type checking instead.
Multiple Inheritance Can Get Genuinely ComplexWhile supported, deep or tangled multiple-inheritance hierarchies can make the method resolution order genuinely hard to reason about, especially in the classic 'diamond problem' shape.
Runtime Errors Instead of Compile-Time ChecksA missing method, wrong attribute name, or forgotten self isn't caught until that specific line actually executes, unlike statically-typed OOP languages that catch many such errors before the program ever runs.
Overuse of Classes for Simple ScriptsPython's flexibility doesn't stop developers from over-engineering small scripts with unnecessary class hierarchies where a handful of plain functions would have been simpler and clearer.

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.

Class Definition
class Dog:Class-level attributesMethod definitions (self, ...)
Instantiation
Dog("Bruno", "Labrador")__new__ allocates the object__init__ initialises its attributes
Instance Namespace
self.name, self.breedUnique per objectChecked FIRST for attribute lookup
Class Namespace
Shared class-level attributesMethod definitionsChecked if not found on the instance
Inheritance Chain (MRO)
Parent class(es)Grandparent class(es)Checked in Method Resolution Order
Base object Type
Root of every Python classProvides default __repr__, __eq__, etc.Final fallback in attribute lookup

Architecture Diagram

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.

๐Ÿ Pythonoop_example.py
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__ automatically

Output

Generic Creature says ... Bruno the Labrador barks: Woof! Animal(Bruno)

Now the classic beginner mistake โ€” forgetting self โ€” reproduced exactly:

๐Ÿ Pythonmissing_self.py
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 given

Practice This Code โ€” Live Editor

Line-by-Line Explanation

  • โ–ถ

    class Dog(Animal): โ€” This is inheritance. Dog automatically gains everything Animal defines, 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 duplicating self.name = name again inside Dog.

  • โ–ถ

    def speak(self): defined again inside Dog โ€” This is polymorphism through method overriding. Calling .speak() on an Animal object and a Dog object runs genuinely different code, even though the method is called identically in both cases.

  • โ–ถ

    def __str__(self): โ€” A dunder method that customises what print(animals[1]) actually displays, letting a custom object integrate naturally with Python's built-in print() function instead of showing a default, unhelpful memory address.

  • โ–ถ

    def bark(): without self โ€” The fix is simply adding self as the first parameter: def bark(self):. Python was already automatically supplying my_dog as an argument the moment my_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.

Encapsulation LevelSyntaxEnforcementTypical Use
Publicself.nameNone โ€” fully openDefault for most straightforward attributes
Protected (convention)self._nameNone โ€” courtesy signal onlyInternal implementation details other devs shouldn't rely on
Private (name-mangled)self.__nameWeak โ€” requires knowing the mangled nameDiscouraging accidental external access more strongly
Property-based@property decorated methodGenuine โ€” runs validation logic on access/assignmentControlled, validated access presented as a plain attribute

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)

Easy

2. What error occurs, and why, if you run: class Cat: def meow(): print('Meow') \n c = Cat() \n c.meow()?

Easy

3. 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())

Medium

4. 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)

Medium

5. Why does this raise an error? class BankAccount: def __init__(self, balance): self.__balance = balance \n acc = BankAccount(100) \n print(acc.__balance)

Hard

6. What does class Shape(ABC): @abstractmethod def area(self): pass followed by shape = Shape() produce?

Medium

7. 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())

Hard

8. What does super().__init__(name, sound='Woof') accomplish inside a subclass's own __init__ method?

Medium

Conclusion โ€” 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.

Your SituationWhat to Focus On
Learning classes for the first timeโœ… class, __init__, self โ€” get the mechanics automatic first
Modelling something with real, persistent stateโœ… A class is likely the right tool โ€” bank account, game entity, user session
Simple, stateless data transformationโš ๏ธ Plain functions are often clearer than an unnecessary class
Sharing behaviour across related typesโœ… Inheritance โ€” but keep hierarchies shallow and genuinely justified
Different types need to respond to the same call differentlyโœ… Polymorphism via method overriding
Need to enforce a required interface for a libraryโœ… abc.ABC with @abstractmethod
Class is really just holding structured dataโš ๏ธ Consider @dataclass instead of a full manual class
Attribute should have validated, controlled accessโœ… @property, not a raw public attribute

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. ๐Ÿ

Frequently Asked Questions (FAQ)