Skip to content

Methods & Properties Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

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.

Read the in-depth guidePython @classmethod vs @staticmethod vs Instance Method — When to Use Each(opens in new tab)
15 of 15

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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel