Abstract Base Classes & Protocols Interview Questions & Answers
Python interview questions on abstract base classes and @abstractmethod, why use ABCs, collections.abc, duck typing vs ABCs, and typing.Protocol for structural/static duck typing with runtime_checkable.
An abstract base class (ABC) is a class that can't be instantiated
directly and defines a set of methods subclasses must implement. You build
one by subclassing abc.ABC and decorating required methods with
@abstractmethod. Python refuses to instantiate any subclass that leaves an
abstract method unimplemented.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): # subclasses MUST implement this
...
Shape() # TypeError — can't instantiate abstract class
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self): # concrete implementation
return 3.14159 * self.r ** 2
Circle(2).area() # 12.566... — works
@abstractmethod turns the "must implement" contract into an enforced,
load-time check rather than a runtime NotImplementedError later. It defines
an interface that subclasses are required to fulfill.
ABCs enforce an interface — they guarantee at instantiation time that
subclasses provide the required methods, catching mistakes early instead of
blowing up deep in your code. They also document intent clearly and integrate
with isinstance checks, so callers can verify capabilities.
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save(self, key, value): ...
@abstractmethod
def load(self, key): ...
class FileStorage(Storage):
def save(self, key, value): ...
# forgot load() ...
FileStorage() # TypeError — flags the missing method immediately
isinstance(FileStorage, type) and issubclass(FileStorage, Storage) # True
Use an ABC when you have a family of classes that must share a contract and
you want that contract enforced. For looser, optional interfaces, plain duck
typing or a Protocol may fit better.
collections.abc provides the standard ABCs for Python's container
protocols — Iterable, Iterator, Sequence, Mapping, Hashable, and
more. You use them two ways: as a base class to inherit mixin methods, and
in isinstance checks to test whether an object supports a protocol.
from collections.abc import Iterable, Sequence, Mapping
isinstance([1, 2], Iterable) # True
isinstance("abc", Sequence) # True
isinstance({}, Mapping) # True
class MyList(Sequence): # inherit the protocol's mixin methods
def __init__(self, data):
self._data = data
def __getitem__(self, i):
return self._data[i]
def __len__(self):
return len(self._data)
# get __contains__, __iter__, __reversed__, index, count for FREE
Subclassing Sequence and implementing just __getitem__ + __len__ gives you
a fully functional sequence. Prefer these standard ABCs over inventing your own
for container-like types.
Duck typing is "if it walks like a duck and quacks like a duck, it's a duck" — Python doesn't check the type, it just tries the operation and works if the needed methods exist. ABCs add an explicit, enforced contract on top, letting you assert membership and catch missing methods up front.
# Duck typing — no declared interface, just call and hope:
def make_it_quack(thing):
thing.quack() # works for ANYTHING with a quack() method
class Dog:
def quack(self): return "woof-quack"
make_it_quack(Dog()) # works — Dog "is" a duck here
# ABC — explicit, checkable contract:
from abc import ABC, abstractmethod
class Duck(ABC):
@abstractmethod
def quack(self): ...
Duck typing is flexible and Pythonic but defers errors to runtime; ABCs trade some flexibility for early enforcement and clear interfaces. Use duck typing for loose coupling, ABCs when you want guarantees.
typing.Protocol enables structural typing ("static duck typing"): a class
satisfies a protocol simply by having the right methods/attributes — no
explicit inheritance needed. Type checkers verify the structure statically,
giving you duck typing's flexibility with static safety. Add
@runtime_checkable to also allow isinstance checks at runtime.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> str: ... # required shape, not inheritance
class Button: # does NOT inherit Drawable
def draw(self) -> str:
return "[Button]"
def render(item: Drawable) -> str: # type checker accepts Button
return item.draw()
render(Button()) # works — structural match
isinstance(Button(), Drawable) # True — thanks to @runtime_checkable
Unlike ABCs, classes don't register or subclass anything — matching the
structure is enough. Note @runtime_checkable only checks method
names/existence, not signatures. Use Protocols for flexible, statically
verified interfaces; use ABCs when you need shared implementation or explicit
registration.
Stack the decorators with @abstractmethod innermost (closest to the
function). It composes with @property, @classmethod, and @staticmethod to
require subclasses to provide those specific member kinds.
from abc import ABC, abstractmethod
class Config(ABC):
@property
@abstractmethod
def name(self) -> str: ... # subclasses must define a `name` property
@classmethod
@abstractmethod
def from_file(cls, path): ... # required classmethod
Rule of thumb: put @abstractmethod directly above the def, with @property/
@classmethod above it — order matters, and getting it wrong silently drops the
abstractness.
MyABC.register(SomeClass) makes SomeClass a virtual subclass: isinstance/
issubclass report True without inheritance — but the ABC does not enforce
that abstract methods exist and provides no mixin methods. It's a pure
"I promise this conforms" assertion.
from abc import ABC, abstractmethod
class Quacker(ABC):
@abstractmethod
def quack(self): ...
class Duck: # no inheritance
def quack(self): return "quack"
Quacker.register(Duck)
issubclass(Duck, Quacker) # True — virtual subclass
isinstance(Duck(), Quacker) # True
# but Python never checks Duck actually has quack()
Rule of thumb: register retrofits third-party classes into an ABC hierarchy for
isinstance checks, but you lose abstractness enforcement and inherited mixins.
Overriding __subclasshook__ lets an ABC decide issubclass/isinstance by
inspecting structure (e.g. "has a __len__?") rather than the inheritance tree.
This is how collections.abc classes recognize unrelated types.
from abc import ABC, abstractmethod
class Sized(ABC):
@abstractmethod
def __len__(self): ...
@classmethod
def __subclasshook__(cls, C):
if cls is Sized:
return any("__len__" in B.__dict__ for B in C.__mro__)
return NotImplemented
isinstance([1, 2], Sized) # True — list has __len__, no registration
Rule of thumb: __subclasshook__ powers duck-typed isinstance (like Iterable/
Hashable); return NotImplemented to fall back to normal subclass logic.
Choose a Protocol when you only need a structural contract and don't control (or don't want to couple) the implementing classes — great for typing third-party or built-in objects. Choose an ABC when you want shared mixin code, explicit registration, or instantiation-time enforcement.
from typing import Protocol
class SupportsClose(Protocol): # any object with close() fits, no inheritance
def close(self) -> None: ...
def cleanup(r: SupportsClose) -> None:
r.close() # files, sockets, custom — all match
Rule of thumb: Protocol = "fits by shape, no coupling, static checks"; ABC = "shared base, enforced at runtime, explicit hierarchy."
NotImplementedError is an exception you raise to signal an unfinished
method. NotImplemented is a singleton value you return from binary
special methods (__eq__, __add__) to tell Python "try the reflected operation."
They are not interchangeable.
class Base:
def render(self):
raise NotImplementedError # subclass must override
class Money:
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented # let Python try other.__eq__
return self.amount == other.amount
Rule of thumb: raise NotImplementedError for abstract-ish stubs; return NotImplemented from operator dunders to allow fallback/reflection.
ABCs work via the ABCMeta metaclass. abc.ABC is just a convenience base class
that already uses that metaclass, so you can write class X(ABC) instead of the
verbose class X(metaclass=ABCMeta). You need the metaclass form when combining with
another metaclass.
from abc import ABC, ABCMeta, abstractmethod
class A(ABC): # the easy, common way
@abstractmethod
def f(self): ...
class B(metaclass=ABCMeta): # equivalent, explicit metaclass
@abstractmethod
def f(self): ...
Rule of thumb: use ABC for everyday abstract classes; drop to
metaclass=ABCMeta only when you must merge it with a custom metaclass.
Yes. @abstractmethod still requires the subclass to override it, but the body
can hold shared logic the subclass calls via super(). This gives a default while
keeping the override mandatory.
from abc import ABC, abstractmethod
class Validator(ABC):
@abstractmethod
def validate(self, x):
if x is None: # shared precondition
raise ValueError("None not allowed")
# subclass:
class Positive(Validator):
def validate(self, x):
super().validate(x) # reuse base checks
return x > 0
Rule of thumb: an abstract method with a body provides reusable scaffolding that
subclasses extend via super() — but the override is still required.
Yes — declare them as annotated class variables in the Protocol body. A class
satisfies the protocol if it has matching attributes (commonly set in __init__),
checked structurally by the type checker.
from typing import Protocol
class Named(Protocol):
name: str # required attribute
def greet(self) -> str: ...
class User:
def __init__(self, name: str):
self.name = name # satisfies `name: str`
def greet(self) -> str:
return f"hi {self.name}"
Rule of thumb: Protocols can specify both attributes and methods — annotate the attribute in the body; no value/assignment needed.
The check fires at instantiation, not definition. Defining an incomplete
subclass is fine; the TypeError only happens when you try to create an
instance. This lets you build deep abstract hierarchies.
from abc import ABC, abstractmethod
class A(ABC):
@abstractmethod
def f(self): ...
class B(A): # OK — still abstract, no error yet
pass
class C(B):
def f(self): return 1
B() # TypeError here — at instantiation
C() # fine
Rule of thumb: abstractness is enforced when you instantiate, so intermediate abstract subclasses are legal and common.
Each container ABC lists abstract methods (you implement) and mixin methods
(you get free). E.g. Mapping needs __getitem__, __len__, __iter__ and then
gives you keys, items, values, get, __contains__, __eq__.
from collections.abc import Mapping
class FrozenDict(Mapping):
def __init__(self, d): self._d = dict(d)
def __getitem__(self, k): return self._d[k] # required
def __iter__(self): return iter(self._d) # required
def __len__(self): return len(self._d) # required
fd = FrozenDict({"a": 1})
fd.get("a"), "a" in fd, list(fd.keys()) # all work for free
Rule of thumb: implement only the few abstract methods an abc lists and inherit the
rest — far less code than building a container by hand.
More Object-Oriented Programming interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.