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__ generatedReaching 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 arguments —
def __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 withoutself, or callingClass.method()without an instance. __str__vs__repr__—__repr__is for developers/debugging (unambiguous),__str__for users.
Interviewer follow-ups
- "How is
@classmethoddifferent from@staticmethod?" —classmethodreceivescls(great for alternative constructors);staticmethodreceives nothing. - "How would you make an attribute read-only?" — expose a
@propertygetter with no setter. - "Composition or inheritance?" — favour composition; see the trade-off in the inheritance guide.
Related guides
- How inheritance works in Python (
super(), MRO, multiple inheritance) - Polymorphism in Python (duck typing, dunder methods,
singledispatch)
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).
Software Engineering Leader & Technical Author · Updated August 26, 2026