December Code

Python OOP interview questions, with answers

Object-oriented Python looks like OOP in Java until an interviewer asks about the parts that differ: self is an ordinary parameter, an attribute can live on the class or on the object, 'private' is a naming convention, and multiple inheritance is settled by a computed order. Those differences are where the questions come from, and most of them are asked as a short class followed by 'what does this print?'. Every answer below uses a small class you can run, and every output shown was produced by running it.

For the language-neutral ideas — polymorphism, encapsulation, abstraction — the OOP pages go deeper; this page is about how Python does them. Then take the free Python diagnostic — ten questions across every Python topic in the bank — to see which of these you can explain but not yet predict.

The questions, with answers

  1. 1.What is self in a Python method, and why is it written explicitly?

    In short: self is the object the method was called on, passed in as the first argument; r.move(5) is shorthand for Robot.move(r, 5), so the method must name that parameter.

    Python has no hidden this. When you write r.move(5), Python looks move up on the class, finds a plain function, and calls it with r as the first argument, exactly as Robot.move(r, 5) does below. The name self is only a convention; what matters is the position. Leaving self out of a definition gives the TypeError 'takes 1 positional argument but 2 were given' the first time the method is called through an object, because the object is always passed. Attributes are created by assigning to self.name, usually in __init__, and methods reach them through self; a bare name inside a method is a local or a global, never an attribute.

    class Robot:
        def __init__(self, x):
            self.x = x
    
        def move(self, dx):
            self.x += dx
    
    r = Robot(1)
    Robot.move(r, 5)
    print(r.x)  # 6
  2. 2.Is __init__ a constructor, and what does __new__ do?

    In short: __new__ creates and returns the object, then __init__ initialises it; __init__ is what people call the constructor, but the object already exists when it runs.

    Calling Celsius(212) makes Python call Celsius.__new__(Celsius, 212) to create the instance and then, if that returned a Celsius, __init__(instance, 212) to fill it in. __init__ must return None; its only job is to set attributes. You rarely override __new__, and interviewers ask about it for the two cases where you must. The first is subclassing an immutable type such as int, float, str or tuple: the value is fixed at creation, so it has to be chosen in __new__, as below. The second is controlling creation itself, as a cache or a singleton does when it returns an existing object instead of a new one.

    class Celsius(float):
        def __new__(cls, f):
            c = (f - 32) * 5 / 9
            return super().__new__(
                cls, c)
    
    t = Celsius(212)
    print(t, isinstance(t, float))
    # 100.0 True
  3. 3.What is the difference between a class attribute and an instance attribute?

    In short: A class attribute is stored once on the class and shared by every instance; an instance attribute belongs to one object, and assigning through an object creates one that hides the class's.

    Looking up an attribute on an object checks the object's own __dict__ first, then its class, then the base classes. So reading a.tax finds the class attribute when the object has none of its own, but a.tax = 0.05 never touches the class: it creates an instance attribute on a alone, which from then on hides the class value for that one object. The trap is a mutable class attribute. A list written in the class body is a single list, and a.stock.append(x) changes it for every instance, because appending is a lookup followed by a method call, not an assignment. Data that belongs to each object goes in __init__, assigned to self.

    class Shop:
        tax = 0.18
        stock = []
    
    a, b = Shop(), Shop()
    a.tax = 0.05
    a.stock.append("pen")
    print(b.tax, b.stock)
    # 0.18 ['pen']
  4. 4.What is the difference between @classmethod and @staticmethod?

    In short: A classmethod receives the class as its first argument and suits alternative constructors; a staticmethod receives nothing implicit and is a plain function kept in the class.

    An ordinary method receives the instance, a classmethod receives the class, conventionally named cls, and a staticmethod receives neither. The standard use for a classmethod is an alternative constructor, such as Temp.from_f below or date.fromisoformat in the standard library. Because it receives cls, calling it on a subclass builds the subclass, which a staticmethod that names the class directly would not. A staticmethod suits a helper that belongs with the class but needs no class or instance state, such as the validity check below. Both can be called on the class or on an instance.

    class Temp:
        def __init__(self, c):
            self.c = c
    
        @classmethod
        def from_f(cls, f):
            c = (f - 32) * 5 / 9
            return cls(c)
    
        @staticmethod
        def valid(c):
            return c >= -273.15
    
    t = Temp.from_f(212)
    print(t.c)  # 100.0
    print(Temp.valid(-300))
    # False
  5. 5.What is the difference between __str__ and __repr__?

    In short: __repr__ is the unambiguous, developer-facing form, ideally code that rebuilds the object; __str__ is the readable form print uses, and it falls back to __repr__.

    print(obj) and str(obj) call __str__, while repr(obj), the interactive prompt and the debugger call __repr__. Containers show their items with repr, so a list of Fee objects prints as [Fee(250)] even though Fee has a __str__. When a class defines only __repr__, str() falls back to it, which is why the usual advice is to always write __repr__ and add __str__ only when a friendlier form is needed. With neither, you get the default <__main__.Fee object at 0x...>. A good __repr__ returns something like Fee(250), which tells the reader how to rebuild the value.

    class Fee:
        def __init__(self, n):
            self.n = n
    
        def __repr__(self):
            return f"Fee({self.n})"
    
        def __str__(self):
            return f"₹{self.n}"
    
    f = Fee(250)
    print(f)        # ₹250
    print(repr(f))  # Fee(250)
  6. 6.How does super() work, and what is the method resolution order?

    In short: super() finds the next class after the current one in the object's method resolution order, which with multiple inheritance is not always the parent the class lists.

    Every class has a method resolution order, shown by Class.__mro__: the class itself, then its bases in the order they are listed, with every class placed before its own bases. Attribute lookup walks that list, and super() continues the walk from the current class for the actual object. With single inheritance, as below, that is simply the parent: call super().__init__() to let the parent set its attributes, then set the subclass's own. With multiple inheritance the next class can be a sibling rather than the parent, which is what lets every __init__ in a hierarchy call super().__init__() and have each run exactly once; the diamond-problem page walks through that case.

    class Animal:
        def __init__(self, name):
            self.name = name
    
    class Dog(Animal):
        def __init__(self, name):
            super().__init__(name)
            self.sound = "woof"
    
    d = Dog("Rex")
    print(d.name, d.sound)
    # Rex woof
    names = [c.__name__
             for c in Dog.__mro__]
    print(names)
    # ['Dog', 'Animal', 'object']
  7. 7.How do you make an attribute private in Python?

    In short: Strictly, you cannot: one leading underscore marks an attribute internal by convention, two trigger name mangling, and @property controls access through a method.

    Python has no access modifiers. A name such as _balance tells other programmers that it is internal and tools respect that, but nothing prevents access. A name with two leading underscores, such as __bal inside class Account, is rewritten by the compiler to _Account__bal, which stops a subclass's own __bal from clashing with it; it is still reachable under the rewritten name, so mangling is about safety in inheritance, not secrecy. When outside code needs controlled access, @property turns a method into something read like an attribute, and without a setter, assignment fails, as below. Adding a setter later lets you validate every assignment without changing any caller.

    class Account:
        def __init__(self):
            self.__bal = 0
    
        @property
        def bal(self):
            return self.__bal
    
    a = Account()
    print(a.bal)  # 0
    print(a._Account__bal)  # 0
    a.bal = 5  # AttributeError
  8. 8.What are dunder methods, and how do they relate to operators?

    In short: Dunder methods such as __len__, __add__ and __eq__ are hooks Python calls for built-in operations, so defining them makes your objects work with len(), + and ==.

    len(v) calls v.__len__(), a + b calls a.__add__(b), a == b calls a.__eq__(b), and a for loop calls __iter__. Defining these is how a class plugs into the language, and it is what operator overloading means in Python. A comparison or arithmetic method should return NotImplemented, not False or an error, when it does not handle the other operand's type, so that Python can try the other operand's method instead. One rule interviewers probe: objects that compare equal must have equal hashes, so a class that defines __eq__ without __hash__ has its __hash__ set to None, and its instances cannot be set members or dict keys until you define one.

    class Vec:
        def __init__(self, x, y):
            self.x, self.y = x, y
    
        def __add__(self, o):
            x = self.x + o.x
            y = self.y + o.y
            return Vec(x, y)
    
        def __len__(self):
            return 2
    
    v = Vec(1, 2) + Vec(3, 4)
    print(v.x, v.y, len(v))
    # 4 6 2
  9. 9.Does Python have interfaces and abstract classes?

    In short: It has abstract base classes — inherit from abc.ABC and mark methods @abstractmethod — and for the rest relies on duck typing; there is no interface keyword.

    Python's default is duck typing: any object with the right methods works, whatever its class, so a function that calls .read() accepts a file, an io.StringIO or a class of your own. When you want the contract checked, an abstract base class does it: a class that inherits from abc.ABC and marks methods with @abstractmethod cannot be instantiated, and neither can a subclass that leaves any of them unimplemented, so the TypeError comes at creation rather than at the first missing call. For checking without inheritance, typing.Protocol describes the methods a type must have and lets a type checker verify them structurally. Together these cover what Java splits into interfaces and abstract classes.

    import abc
    
    class Shape(abc.ABC):
        @abc.abstractmethod
        def area(self): ...
    
    class Square(Shape):
        def __init__(self, s):
            self.s = s
    
        def area(self):
            return self.s ** 2
    
    print(Square(3).area())  # 9
    Shape()  # TypeError

How the diagnostic asks it

One question from the Python bank, exactly as a sitting would show it. The bank has 5 on oop in python and 30 across Python.

OOP in Python · easyPY-015

What does this code print?

class Counter:
    count = 0

a = Counter()
b = Counter()
a.count = 5
print(Counter.count, a.count, b.count)
  1. 15 5 5
  2. 20 5 0correct
  3. 30 5 5
  4. 45 5 0

a.count = 5 creates a new attribute on the object a, which hides the class attribute for a only. Counter.count and b.count still read the class attribute, 0, so the output is 0 5 0. 5 5 5 assumes the assignment changed the class attribute, 0 5 5 assumes b shares a's new value, and 5 5 0 assumes the class attribute changed while b kept the old one.

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 Python questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated