Methods & Properties Interview Questions & Answers
Python interview questions on instance vs classmethod vs staticmethod, classmethods as alternative constructors, the @property getter/setter, why properties beat Java-style accessors, and computed/read-only properties.
An instance method takes self and operates on a specific object. A
@classmethod takes cls (the class, not an instance) and works on
class-level state or builds instances. A @staticmethod takes
neither — it's a plain function namespaced inside the class, with no access
to instance or class state.
class Pizza:
base_price = 10
def __init__(self, toppings):
self.toppings = toppings
def total(self): # instance method — uses self
return self.base_price + len(self.toppings)
@classmethod
def margherita(cls): # classmethod — uses cls
return cls(["mozzarella", "basil"])
@staticmethod
def is_valid_topping(name): # staticmethod — no self/cls
return name.isalpha()
Pizza.is_valid_topping("ham") # True — no instance needed
Pizza.margherita().total() # 12
Use an instance method for per-object behavior, a classmethod for class-aware logic (e.g. alternative constructors), and a staticmethod for a related helper that happens to live on the class.
A classmethod receives cls, so it can build and return a new instance from
a different input format — giving you multiple named constructors beyond
__init__. Using cls (not the hard-coded class name) means subclasses get
the right type automatically.
class Date:
def __init__(self, year, month, day):
self.year, self.month, self.day = year, month, day
@classmethod
def from_string(cls, text):
y, m, d = map(int, text.split("-"))
return cls(y, m, d) # returns the (sub)class instance
@classmethod
def today(cls):
import datetime
t = datetime.date.today()
return cls(t.year, t.month, t.day)
Date.from_string("2026-06-18") # alternative constructor
Date.today()
This is the idiomatic Python pattern (dict.fromkeys, datetime.fromtimestamp)
for "make one of these, but from X" — clearer than overloading __init__ with
flags.
@property turns a method into a managed attribute — you access it like
obj.x (no parentheses), but a method runs behind the scenes. The matching
@x.setter runs on assignment, letting you add validation without changing
the public interface.
class Account:
def __init__(self, balance):
self._balance = balance # "private" backing field
@property
def balance(self): # getter — runs on read
return self._balance
@balance.setter
def balance(self, value): # setter — runs on write
if value < 0:
raise ValueError("balance cannot be negative")
self._balance = value
acc = Account(100)
acc.balance # 100 — looks like an attribute, calls the getter
acc.balance = 50 # calls the setter (validated)
acc.balance = -1 # ValueError
The convention is a _name backing attribute behind a public property. Omit the
setter to make the property read-only.
In Python you start with a plain attribute and only convert it to a
@property later if you need validation or computation — without changing
the call sites. So you avoid the Java habit of pre-emptively writing
getX()/setX() "just in case". The public API stays obj.x.
# Start simple — public attribute:
class Circle:
def __init__(self, radius):
self.radius = radius
c = Circle(5)
c.radius # direct access — no ceremony
# Later, add validation transparently — callers don't change:
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("radius must be positive")
self._radius = value
This is the "uniform access principle": callers can't tell whether obj.x is a
stored value or computed. Don't write getters/setters upfront — reach for a
property only when behavior is needed.
A property with only a getter (no setter) is read-only — assigning to it
raises AttributeError. A computed property derives its value from other
attributes on each access, so it stays in sync automatically rather than being
stored.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self): # computed — derived on each read
return self.width * self.height
r = Rectangle(3, 4)
r.area # 12 — computed
r.width = 5
r.area # 20 — automatically reflects the change
r.area = 99 # AttributeError — read-only (no setter)
For an expensive computation you only want to run once, use
functools.cached_property instead, which caches the result on first access.
Use a plain read-only property for cheap derived values.
@cached_property computes the value on first access, then stores it in the
instance __dict__ so later reads are free — no recomputation. A plain @property
recomputes every time. The cache lives until you del the attribute.
from functools import cached_property
class Dataset:
def __init__(self, rows): self.rows = rows
@cached_property
def stats(self):
print("computing...") # runs only once
return sum(self.rows) / len(self.rows)
d = Dataset([1, 2, 3])
d.stats # computing... -> 2.0
d.stats # 2.0 (no recompute)
del d.stats # clears the cache; next access recomputes
Rule of thumb: use cached_property for expensive, stable derived values; stick
with @property when the underlying data changes and must stay fresh. Note it needs
a writable __dict__ (won't work with __slots__ unless you add __dict__).
The deleter runs when you do del obj.x, letting you hook into attribute
deletion (cleanup, resetting a cache, or forbidding deletion). It's the third piece
alongside getter and setter.
class Resource:
def __init__(self): self._handle = "open"
@property
def handle(self): return self._handle
@handle.deleter
def handle(self):
print("closing")
self._handle = None
r = Resource()
del r.handle # prints "closing"; runs cleanup logic
Rule of thumb: add a @x.deleter when del obj.x should trigger behavior; omit it
and deletion raises AttributeError.
property is just a built-in data descriptor — a class implementing __get__,
__set__, __delete__. That's why properties are defined on the class (not the
instance) and why they intercept attribute access. You can write your own descriptor
to reuse get/set logic across many attributes.
class Positive: # reusable descriptor
def __set_name__(self, owner, name): self.name = "_" + name
def __get__(self, obj, owner):
return getattr(obj, self.name)
def __set__(self, obj, value):
if value <= 0: raise ValueError("must be positive")
setattr(obj, self.name, value)
class Product:
price = Positive() # one descriptor, reusable
weight = Positive()
Rule of thumb: a property is a per-attribute descriptor; write a custom descriptor class when the same validation/logic should apply to many attributes.
Because the property is defined on the class, any assignment to self.x —
including inside __init__ — goes through the setter. This is useful: validation
runs even at construction, so you don't duplicate checks.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # goes through the setter -> validated
@property
def celsius(self): return self._c
@celsius.setter
def celsius(self, value):
if value < -273.15: raise ValueError("below absolute zero")
self._c = value
Temperature(-300) # ValueError, raised from __init__
Rule of thumb: assign to the public property name in __init__ (not the _backing
field) so constructor values get the same validation as later writes.
Yes — both are accessible via an instance or the class. A classmethod always
receives the class as cls regardless; a staticmethod receives nothing automatic
either way. Calling on an instance doesn't pass self.
class C:
@classmethod
def cm(cls): return cls.__name__
@staticmethod
def sm(x): return x * 2
c = C()
c.cm() # 'C' — cls is C, even called on instance
C.cm() # 'C'
c.sm(5) # 10 — self not passed
C.sm(5) # 10
Rule of thumb: class/static methods work the same called on the class or an instance; the difference is only what implicit first argument (if any) they receive.
A bound method keeps a reference to its instance, so storing obj.method as a
callback keeps obj alive (prevents garbage collection) and always operates on that
instance. This is usually desired, but can cause unexpected lifetime extension.
class Handler:
def __init__(self, name): self.name = name
def on_event(self): return f"handled by {self.name}"
h = Handler("A")
callbacks = [h.on_event] # bound method holds h
del h # h NOT collected — callback still references it
callbacks[0]() # 'handled by A'
Rule of thumb: storing obj.method pins obj in memory; for caches/observers that
shouldn't extend lifetime, use weakref.WeakMethod.
A classmethod uses cls, so when a subclass calls the factory it builds an
instance of the subclass. A staticmethod hard-codes the class name, so it always
builds the base type — breaking subclassing.
class Shape:
@classmethod
def square(cls, size):
return cls(size, size) # cls -> correct subclass
@staticmethod
def square_bad(size):
return Shape(size, size) # always Shape, ignores subclass
def __init__(self, w, h): self.w, self.h = w, h
class Tile(Shape): pass
type(Tile.square(5)) # Tile — polymorphic
type(Tile.square_bad(5)) # Shape — wrong
Rule of thumb: use classmethod for constructors so they respect subclasses; reserve
staticmethod for helpers that genuinely never need the class.
Make it a property when it's a cheap, attribute-like value with no side
effects and no arguments (obj.area). Make it a method when it takes arguments,
is expensive, or performs an action (obj.calculate(), obj.save()).
class Circle:
def __init__(self, r): self.r = r
@property
def area(self): # noun, cheap, no args -> property
return 3.14159 * self.r ** 2
def scaled(self, factor): # takes an arg -> method
return Circle(self.r * factor)
Rule of thumb: properties read like nouns/data (obj.x); methods read like verbs/
actions (obj.do()). Don't hide expensive or mutating work behind a property.
Redefine the whole property in the subclass (you can't override just the getter or
setter in isolation). To reuse the parent's logic, call it via
SuperClass.prop.fget(self).
class Base:
@property
def value(self): return 10
class Child(Base):
@property
def value(self):
return Base.value.fget(self) * 2 # reuse parent getter
Child().value # 20
Rule of thumb: overriding a property means redefining it; reach into
Base.prop.fget/fset when you want to build on the parent's accessor.
They're equivalent for instance methods. obj.method() looks up method on the
class via the descriptor protocol, binds obj as self, and calls it.
Class.method(obj) does the binding manually. The dotted form is just sugar.
class C:
def greet(self, name): return f"hi {name}"
c = C()
c.greet("Ada") # 'hi Ada' — self=c automatically
C.greet(c, "Ada") # 'hi Ada' — pass self explicitly
Rule of thumb: obj.method(args) ≡ type(obj).method(obj, args); the explicit form
is occasionally handy to call a specific class's version directly.
More Object-Oriented Programming interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.