InterviewsVector

Object-Oriented Programming in Python: Classes, Objects & the Four Pillars (Interview Guide)

Quick answer

A class is a blueprint that bundles data (attributes) and behaviour (methods); an object is an instance of it. Define a class with the class keyword, initialise per-instance state in __init__(self, ...), and create objects by calling the class. Python OOP rests on four pillars — encapsulation, abstraction, inheritance, and polymorphism — and adds Pythonic tools like @property, dunder methods, and @dataclass to keep classes small.

Short answer: A class is a blueprint that bundles data (attributes) and behaviour (methods); an object is an instance of it. You initialise per-instance state in __init__(self, ...) and create objects by calling the class. Python OOP rests on four pillars — encapsulation, abstraction, inheritance, polymorphism — plus Pythonic tools like @property and @dataclass.

OOP questions are a staple of Python interviews, and the difference between a junior and senior answer is rarely the definition of a class — it's knowing the Pythonic way OOP actually works: duck typing over interfaces, @property over getters/setters, @dataclass over boilerplate.

Classes and objects

A class defines structure and behaviour; an object is a live instance holding its own state.

class Person:
    species = "Homo sapiens"          # class attribute — shared by all instances
 
    def __init__(self, name, age):    # constructor: runs when you create an object
        self.name = name              # instance attributes — unique per object
        self.age = age
 
    def greet(self):                  # instance method: `self` is the object
        return f"Hi, I'm {self.name}, {self.age}."
 
p = Person("Ada", 36)
print(p.greet())        # Hi, I'm Ada, 36.
print(p.species)        # Homo sapiens (read from the class)

self is the instance the method was called on — Python passes it automatically, so p.greet() really means Person.greet(p).

Class vs instance attributes is a favourite trap. species lives on the class and is shared; name/age live on each object. A mutable class-level default (tags = []) shared across instances — or a mutable default argument def __init__(self, tags=[]) — leads to every object mutating the same list. Use None and create a fresh list inside __init__.

Instance vs class vs static methods

They differ by what they receive:

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
 
    def to_fahrenheit(self):                 # instance method — needs the object
        return self.celsius * 9 / 5 + 32
 
    @classmethod
    def from_fahrenheit(cls, f):             # class method — alternative constructor
        return cls((f - 32) * 5 / 9)
 
    @staticmethod
    def is_freezing(celsius):                # static — no self/cls; just grouped here
        return celsius <= 0

@classmethod receives the class (cls) and is the idiomatic way to write alternative constructors. @staticmethod receives nothing and is just a function namespaced on the class.

Encapsulation: @property, not getters/setters

Python has no truly private members — a single leading underscore (_balance) is a "don't touch" convention, and double underscore (__balance) triggers name-mangling, not real privacy. Instead of Java-style getters/setters, use @property to add validation without changing how callers read the attribute:

class Account:
    def __init__(self, balance):
        self._balance = balance
 
    @property
    def balance(self):
        return self._balance
 
    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("balance cannot be negative")
        self._balance = value
 
acct = Account(100)
acct.balance = 150      # goes through the setter (validated)

Less boilerplate: @dataclass and dunder methods

Dunder ("double underscore") methods let your objects work with Python's built-ins — __str__ for print, __eq__ for ==, __len__ for len(). @dataclass generates __init__, __repr__, and __eq__ for you:

from dataclasses import dataclass
 
@dataclass
class Point:
    x: float
    y: float
 
Point(1, 2) == Point(1, 2)   # True — __eq__ generated for you
print(Point(1, 2))           # Point(x=1, y=2) — __repr__ generated

Reaching for @dataclass in an interview signals you write modern Python.

The four pillars, the Python way

  • Encapsulation — bundle state with the methods that manage it; control access with _/@property.
  • Abstraction — expose a simple interface, hide the detail; enforce it with abstract base classes.
  • Inheritance — derive classes to reuse and extend behaviour → deep dive: How inheritance works in Python (including super() and the MRO).
  • Polymorphism — the same operation behaving differently by type, mostly via duck typing → deep dive: Polymorphism in Python.

Common interview traps

  • Mutable default argumentsdef __init__(self, items=[]) shares one list across instances.
  • Class vs instance attributes — mutating a shared class attribute affects every object.
  • Forgetting self — defining a method without self, or calling Class.method() without an instance.
  • __str__ vs __repr____repr__ is for developers/debugging (unambiguous), __str__ for users.

Interviewer follow-ups

  • "How is @classmethod different from @staticmethod?"classmethod receives cls (great for alternative constructors); staticmethod receives nothing.
  • "How would you make an attribute read-only?" — expose a @property getter with no setter.
  • "Composition or inheritance?" — favour composition; see the trade-off in the inheritance guide.

Sources

Key takeaways

  • A class is a blueprint; an object is an instance. __init__ initialises per-instance state, self is that instance.
  • The four pillars: encapsulation, abstraction, inheritance, polymorphism — know how each looks in Python specifically.
  • Instance, class (@classmethod), and static (@staticmethod) methods differ by what they receive: self, cls, or nothing.
  • @property gives controlled attribute access without changing the call site; @dataclass removes __init__/__repr__/__eq__ boilerplate.
  • A mutable default argument (def __init__(self, items=[])) is shared across instances — a classic Python OOP trap.

Frequently asked questions

What is the difference between a class and an object in Python?

A class is the blueprint that defines attributes and methods. An object is a concrete instance created from that blueprint, with its own state. One Person class can produce many Person objects, each holding different name and age values.

What does self mean in Python?

self is the conventional name for the first parameter of an instance method — a reference to the specific object the method was called on. Python passes it automatically when you call obj.method(); you use it to read and write that object's attributes.

What are the four pillars of OOP in Python?

Encapsulation (bundling state with the methods that manage it, and controlling access), abstraction (exposing a simple interface and hiding detail, e.g. via abstract base classes), inheritance (deriving classes from others to reuse behaviour), and polymorphism (the same call behaving differently by object type, largely via duck typing in Python).

By Mohammad Wasi

Software Engineering Leader & Technical Author · Updated August 26, 2026


Related Posts