Advanced Python Interview Questions and Answers

๐Ÿ“‹ Table of Contents โ–พ
  1. Questions & Answers
  2. 📝 Knowledge Check

🐍 Advanced Python Interview Questions

This lesson targets mid-to-senior Python roles. Topics include metaclasses, descriptors, asyncio, dataclasses, type hints, memory management, concurrency, testing with pytest, design patterns, collections, itertools, and property. These questions separate Python developers from those who architect with it.

Questions & Answers

01 What are metaclasses in Python?

Advanced OOP A metaclass is the class of a class โ€” it controls how classes are created. Just as an instance is created by its class, a class is created by its metaclass. The default metaclass is type.

class SingletonMeta(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Database(metaclass=SingletonMeta):
    def __init__(self, url): self.url = url

db1 = Database("postgresql://...")
db2 = Database("other-url")
db1 is db2   # True -- same instance every time

# Class creation hook
class TrackerMeta(type):
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        print(f"Class {name!r} created")
        return cls

# Common real-world uses: ORMs, API frameworks, ABCMeta, singletons
# Rule: if you are not writing a framework, you probably do not need a metaclass

02 What are descriptors? Explain __get__, __set__, __delete__.

Advanced OOP A descriptor is any object defining __get__, __set__, or __delete__. Properties, class methods, static methods, and functions are all descriptors internally.

class Validator:
    """Data descriptor that validates before storing."""
    def __set_name__(self, owner, name):
        self.name = name          # called when class is defined

    def __get__(self, obj, objtype=None):
        if obj is None: return self  # accessed on the class itself
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f"{self.name} must be numeric")
        if value < 0:
            raise ValueError(f"{self.name} cannot be negative")
        obj.__dict__[self.name] = value

class Product:
    price = Validator()
    stock = Validator()
    def __init__(self, price, stock):
        self.price = price       # triggers Validator.__set__
        self.stock = stock

p = Product(9.99, 100)
p.price = -1    # ValueError

03 How does asyncio work in Python? What is the event loop?

Async asyncio provides single-threaded cooperative concurrency. Coroutines (async def) voluntarily yield control at await points, letting the event loop run others while I/O is pending.

import asyncio, httpx

async def fetch_user(client, user_id):
    r = await client.get(f"/users/{user_id}")  # yields control during I/O
    return r.json()

# Sequential -- total ~400ms
async def sequential():
    async with httpx.AsyncClient() as c:
        u1 = await fetch_user(c, 1)   # wait 200ms
        u2 = await fetch_user(c, 2)   # wait another 200ms

# Concurrent -- total ~200ms
async def concurrent():
    async with httpx.AsyncClient() as c:
        u1, u2, u3 = await asyncio.gather(
            fetch_user(c,1), fetch_user(c,2), fetch_user(c,3)
        )   # all start together; wait for the slowest

asyncio.run(concurrent())

# Timeout (Python 3.11+)
async with asyncio.timeout(5.0):
    result = await slow_operation()

04 What are dataclasses and how do they compare to NamedTuple?

Data Classes

from dataclasses import dataclass, field

@dataclass
class Product:
    name:  str
    price: float
    stock: int = 0
    tags:  list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.price < 0: raise ValueError("Negative price")

p = Product("Widget", 9.99)
print(p)   # Product(name='Widget', price=9.99, stock=0, tags=[])

# Frozen (immutable)
@dataclass(frozen=True)
class Point: x: float; y: float

# NamedTuple -- immutable, tuple-based, indexable, unpackable
from typing import NamedTuple
class Coord(NamedTuple):
    lat: float; lon: float
c = Coord(51.5, -0.1)
c[0]           # 51.5 (indexable like a tuple)
lat, lon = c   # unpackable

# Dataclass: mutable, full OOP, not indexable
# NamedTuple: immutable, tuple behaviour, slightly lighter

05 What are type hints and how does the typing module work?

Type System Type hints annotate expected types. Python does NOT enforce them at runtime โ€” they are for mypy/pyright, IDEs, and documentation.

from typing import Optional, TypeVar
from collections.abc import Sequence

T = TypeVar("T")

def greet(name: str, times: int = 1) -> str:
    return (name + " ") * times

def find_user(uid: int) -> Optional[dict]: ...

def first(items: Sequence[T]) -> T: return items[0]

# Python 3.10+ union syntax (cleaner)
def process(v: int | str | None) -> None: ...

# TypedDict -- typed dict shape
from typing import TypedDict
class UserDict(TypedDict):
    id: int; name: str; email: str

# Protocol -- structural subtyping (duck typing + safety)
from typing import Protocol
class Drawable(Protocol):
    def draw(self) -> None: ...

# Run mypy: mypy --strict mymodule.py

06 How does Python memory management work? What is reference counting?

Memory CPython uses reference counting (primary) and a cyclic garbage collector (for reference cycles).

import sys, gc, weakref

x = [1,2,3]   # refcount=1
y = x          # refcount=2
del x          # refcount=1
y = None       # refcount=0 -- freed immediately

# Reference cycles -- refcounting cannot handle these
a = []; b = []
a.append(b); b.append(a)  # a and b reference each other
del a, b   # refcounts drop to 1, not 0 -- LEAK without GC
gc.collect()               # cyclic GC frees the cycle

# Weak references -- don't prevent GC
import weakref
obj = SomeExpensiveObject()
ref = weakref.ref(obj)
del obj        # obj is collected even though ref holds a reference
ref()          # returns None -- object was collected

# Generational GC (gen 0, 1, 2 -- young to old)
gc.get_threshold()   # (700, 10, 10) default

07 What are Python concurrency tools? When do you use threading vs multiprocessing vs asyncio?

Concurrency

import threading, multiprocessing, asyncio
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

# THREADING -- I/O-bound; shared memory; GIL released during I/O
with ThreadPoolExecutor(max_workers=10) as ex:
    results = list(ex.map(download_url, urls))

# MULTIPROCESSING -- CPU-bound; separate processes; no GIL
with ProcessPoolExecutor() as ex:
    results = list(ex.map(heavy_crunch, data_chunks))

# ASYNCIO -- I/O-bound; single thread; highest scalability
async def main():
    async with httpx.AsyncClient() as c:
        results = await asyncio.gather(*[fetch(c,u) for u in urls])

# Decision guide:
# CPU-bound?               use multiprocessing (bypass GIL)
# I/O-bound, few tasks?    use threading (simple, shared memory)
# I/O-bound, many tasks?   use asyncio (highest concurrency, async libraries)

# Shared state -- always use locks with threading
lock = threading.Lock()
with lock:
    shared_counter += 1

08 What are Python abstract base classes (ABCs)?

OOP

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perimeter(self) -> float: ...

    # Concrete method shared by all subclasses
    def describe(self): return f"Area={self.area():.2f}"

Shape()    # TypeError: Can't instantiate abstract class

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self):      return 3.14159 * self.r**2
    def perimeter(self): return 2 * 3.14159 * self.r

# Virtual subclass registration (duck typing + ABC)
class LegacyShape:
    def area(self): return 0.0
    def perimeter(self): return 0.0

Shape.register(LegacyShape)
isinstance(LegacyShape(), Shape)  # True -- without inheriting

09 How does Python MRO work with multiple inheritance?

OOP Python uses the C3 Linearisation algorithm for Method Resolution Order โ€” ensuring consistent, predictable lookup avoiding the Diamond Problem.

class A:
    def hello(self): return "A"

class B(A):
    def hello(self): return "B"

class C(A):
    def hello(self): return "C"

class D(B, C): pass

# MRO: D -> B -> C -> A -> object
print(D.__mro__)
D().hello()   # "B" -- B comes before C in MRO

# super() respects MRO -- calls NEXT class in MRO
class B(A):
    def hello(self): print("B"); super().hello()

class C(A):
    def hello(self): print("C"); super().hello()

class D(B, C):
    def hello(self): print("D"); super().hello()

D().hello()   # D B C A (cooperative multiple inheritance)

# Mixin pattern -- small reusable behaviours
class LogMixin:
    def log(self, msg): print(f"[{type(self).__name__}] {msg}")

class MyService(LogMixin, BaseService):
    def process(self):
        self.log("Processing")

10 What are __slots__ in Python? When do you use them?

Memory __slots__ replaces the per-instance __dict__ with a fixed-size array, reducing memory usage and speeding up attribute access. Use when creating millions of instances.

import sys

class PointDict:
    def __init__(self, x, y): self.x, self.y = x, y

class PointSlot:
    __slots__ = ("x", "y")
    def __init__(self, x, y): self.x, self.y = x, y

p1 = PointDict(1.0, 2.0)
p2 = PointSlot(1.0, 2.0)
sys.getsizeof(p1)   # ~56 bytes (dict overhead)
sys.getsizeof(p2)   # ~48 bytes (slot array)

# Limitations:
p2.z = 3.0   # AttributeError -- cannot add arbitrary attributes
# Inheritance: subclass must also define __slots__ for savings
# Multiple inheritance: all parents must use __slots__

# Use when: creating millions of instances (ML, simulations, data processing)
# Do not use for: general-purpose classes where flexibility matters

11 How does functools.lru_cache work?

Performance lru_cache memoises a function’s results โ€” caches the return value for each unique set of arguments. LRU = Least Recently Used eviction when the cache is full.

from functools import lru_cache, cache, cached_property

@lru_cache(maxsize=256)
def fibonacci(n):
    if n < 2: return n
    return fibonacci(n-1) + fibonacci(n-2)

fibonacci(50)               # computed once O(n)
fibonacci(50)               # instant (cached)
fibonacci.cache_info()      # CacheInfo(hits=49, misses=51, ...)
fibonacci.cache_clear()

# @cache -- unbounded (Python 3.9+), same as lru_cache(maxsize=None)
@cache
def fib(n):
    if n < 2: return n
    return fib(n-1) + fib(n-2)

# REQUIREMENT: all arguments must be hashable
@lru_cache
def process(data: tuple): return sum(data)   # tuple is hashable
# process([1,2,3])   -- TypeError: list is not hashable

# cached_property -- compute once per instance, then store
from functools import cached_property
class DataAnalyser:
    def __init__(self, data): self.data = data

    @cached_property
    def mean(self): return sum(self.data) / len(self.data)  # computed once

12 What are Python’s itertools and how do you use them?

Standard Library itertools provides fast, memory-efficient lazy iterator building blocks.

from itertools import (
    count, cycle, repeat,
    chain, islice, takewhile, dropwhile,
    combinations, permutations, product,
    groupby, accumulate, zip_longest
)

# Infinite iterators
list(islice(count(0), 5))       # [0,1,2,3,4]

# Slicing / filtering
list(takewhile(lambda x: x<5, [1,3,5,7]))  # [1,3]
list(dropwhile(lambda x: x<5, [1,3,5,7]))  # [5,7]

# Combining
list(chain([1,2],[3,4],[5]))    # [1,2,3,4,5]

# Combinatorics
list(combinations("ABC", 2))   # [('A','B'),('A','C'),('B','C')]
list(permutations("AB", 2))    # [('A','B'),('B','A')]
list(product("AB", repeat=2))  # [('A','A'),('A','B'),('B','A'),('B','B')]

# Accumulation
list(accumulate([1,2,3,4,5]))  # [1,3,6,10,15] running sum

# groupby (data must be pre-sorted)
from itertools import groupby
data = sorted([("M","Alice"),("F","Bob"),("M","Charlie")], key=lambda x:x[0])
for gender, group in groupby(data, key=lambda x: x[0]):
    print(gender, list(group))

13 What are Python’s collections module classes?

Standard Library

from collections import Counter, defaultdict, deque, namedtuple, ChainMap

# Counter -- count hashable objects
c = Counter("abracadabra")             # {'a':5,'b':2,'r':2,'c':1,'d':1}
c.most_common(3)                       # top 3 most common
Counter([1,2,2]) + Counter([1,2])      # add counts

# defaultdict -- auto-create missing keys
groups = defaultdict(list)
for word in ["apple","ant","banana"]:
    groups[word[0]].append(word)       # no KeyError

# deque -- O(1) append/pop from BOTH ends
q = deque(maxlen=5)                    # auto-evict oldest
q.appendleft(1); q.append(2)

# namedtuple -- immutable record with named fields
Point = namedtuple("Point", ["x","y","z"])
p = Point(1,2,3)
p.x                 # 1
p._replace(z=5)     # new tuple with z=5

# ChainMap -- view across multiple dicts (first found wins)
defaults = {"theme":"light","lang":"en"}
user     = {"theme":"dark"}
config   = ChainMap(user, defaults)
config["theme"]    # "dark" (user override)
config["lang"]     # "en" (from defaults)

14 How do you write tests with pytest? What are fixtures?

Testing

import pytest
from unittest.mock import MagicMock, patch

def test_add():
    assert 1 + 1 == 2

def test_raises():
    with pytest.raises(ValueError, match="negative"):
        raise ValueError("negative value")

# Parametrised tests -- one test function, many inputs
@pytest.mark.parametrize("n,expected", [(0,0),(1,1),(10,100),(-2,4)])
def test_square(n, expected):
    assert n**2 == expected

# Fixtures -- reusable setup/teardown
@pytest.fixture
def sample_user():
    return {"id":1, "name":"Alice", "email":"alice@test.com"}

@pytest.fixture
def db():
    conn = create_test_db()
    yield conn          # code after yield = teardown
    conn.close()

def test_user(db, sample_user):   # fixtures injected by name
    db.save(sample_user)
    assert db.find(1) is not None

# Scope: function (default), class, module, session
@pytest.fixture(scope="session")
def expensive_resource(): return setup_once()

# Mocking
def test_sends_email(mocker):
    mock_send = mocker.patch("mymodule.send_email")
    process_order(1)
    mock_send.assert_called_once()

15 What is Python’s property decorator and how does it work?

OOP

class BankAccount:
    def __init__(self, owner: str, balance: float = 0.0):
        self._owner   = owner
        self._balance = balance

    @property
    def balance(self) -> float:         # getter -- read only
        return self._balance

    @property
    def owner(self) -> str:
        return self._owner

    @owner.setter                         # setter for owner
    def owner(self, name: str):
        if not name.strip(): raise ValueError("Name cannot be empty")
        self._owner = name.strip()

    def deposit(self, amount: float):
        if amount <= 0: raise ValueError("Must be positive")
        self._balance += amount

    @property
    def formatted_balance(self) -> str:
        return f"GBP {self._balance:,.2f}"

acc = BankAccount("Alice", 1000.0)
acc.balance           # 1000.0 (calls getter)
acc.balance = 500     # AttributeError -- no setter (intentional)
acc.owner = "Bob"     # calls setter, validates

16 What is Python’s Enum class?

Standard Library

from enum import Enum, IntEnum, Flag, auto, unique

class Colour(Enum):
    RED   = 1
    GREEN = 2
    BLUE  = 3

Colour.RED.name    # "RED"
Colour.RED.value   # 1
Colour(1)          # Colour.RED -- lookup by value
list(Colour)       # all members

# auto() -- automatic values
class Direction(Enum):
    NORTH = auto()   # 1
    SOUTH = auto()   # 2

# @unique -- raises if duplicate values
@unique
class Status(str, Enum):   # str mixin: Status.ACTIVE == "active"
    PENDING   = "pending"
    ACTIVE    = "active"

# Flag -- combinable with bitwise operators
class Permission(Flag):
    READ    = auto()
    WRITE   = auto()
    EXECUTE = auto()
    ALL     = READ | WRITE | EXECUTE

p = Permission.READ | Permission.WRITE
Permission.READ in p     # True
Permission.EXECUTE in p  # False

17 What is pathlib and how does it compare to os.path?

Standard Library

from pathlib import Path

p = Path.home() / "documents" / "report.pdf"
p = Path.cwd() / "config.json"   # / operator joins paths

# Inspect
p.name          # "report.pdf"
p.stem          # "report"
p.suffix        # ".pdf"
p.parent        # Path("/home/user/documents")
p.is_file()     # True/False
p.exists()      # True/False

# Glob -- find files
for py in Path(".").glob("**/*.py"): print(py)  # recursive

# Read / write
text = p.read_text(encoding="utf-8")
p.write_text("content")
data = p.read_bytes()

# File system operations
p.mkdir(parents=True, exist_ok=True)  # create directory
p.rename(new_path)                     # rename/move
p.unlink(missing_ok=True)             # delete

# os.path equivalent (older style, still common)
import os.path
os.path.join("/home", "user", "file.txt")  # string-based, verbose

18 What is structured logging with Python’s logging module?

Standard Library

import logging
import logging.config

# Per-module logger (best practice)
logger = logging.getLogger(__name__)   # name = "mypackage.mymodule"

logger.debug("Trace info")
logger.info("Normal operation")
logger.warning("Something unexpected")
logger.error("Error occurred")
logger.critical("System failure")

# Log with exception traceback
try: 1/0
except ZeroDivisionError:
    logger.exception("Division failed")  # includes full traceback

# Configuration
logging.config.dictConfig({
    "version": 1,
    "handlers": {
        "console": {"class":"logging.StreamHandler","level":"INFO"},
        "file": {"class":"logging.handlers.RotatingFileHandler",
                 "filename":"app.log","maxBytes":10_485_760,"backupCount":5}
    },
    "root": {"level":"DEBUG","handlers":["console","file"]},
    "loggers": {
        "httpx": {"level":"WARNING"},    # silence noisy library
    }
})

19 How does Python’s pickle module work? What are the security risks?

Serialisation

import pickle

data = {"name":"Alice","scores":[95,87],"active":True}

# Serialise
blob = pickle.dumps(data)            # bytes
with open("data.pkl","wb") as f: pickle.dump(data, f)

# Deserialise
loaded = pickle.loads(blob)          # same Python object
with open("data.pkl","rb") as f: loaded = pickle.load(f)

# Custom class support
class Config:
    def __getstate__(self):          # called on pickle.dumps
        state = self.__dict__.copy()
        del state["_live_connection"] # exclude unpicklable
        return state
    def __setstate__(self, state):   # called on pickle.loads
        self.__dict__.update(state)
        self._live_connection = None  # reinitialise

# SECURITY WARNING
# pickle.loads() can execute ARBITRARY PYTHON CODE
# NEVER unpickle data from an untrusted source

# Safe alternatives:
# json.dumps/loads    -- text, no custom classes
# msgpack             -- binary, fast, language-agnostic
# Pydantic model_dump -- typed, validated, safe

20 What are Python design patterns and common Pythonic idioms?

Patterns

# Factory pattern
class Shape:
    @classmethod
    def create(cls, shape_type, **kwargs):
        registry = {"circle": Circle, "rectangle": Rectangle}
        return registry[shape_type](**kwargs)

# Strategy pattern -- pass behaviour as callable
def process(data, strategy=sorted):
    return strategy(data)
process([3,1,2])        # default
process([3,1,2], reversed)  # custom

# Observer / event system
class EventEmitter:
    def __init__(self): self._listeners = {}
    def on(self, event, cb): self._listeners.setdefault(event,[]).append(cb)
    def emit(self, event, *args): [cb(*args) for cb in self._listeners.get(event,[])]

# Fluent interface
class QueryBuilder:
    def __init__(self): self._filters=[]; self._limit=None
    def filter(self, c): self._filters.append(c); return self
    def limit(self, n): self._limit=n; return self

query = QueryBuilder().filter("active=1").filter("age>18").limit(10)

# EAFP -- Easier to Ask Forgiveness than Permission (Pythonic)
try:
    value = d["key"]       # try first, handle on failure
except KeyError:
    value = default        # more Pythonic than: if "key" in d: value = d["key"]

21 What is Python’s __init__.py and how does package structure affect imports?

Modules

# mypackage/
#   __init__.py     <-- makes directory a package; executed on import
#   models.py
#   services/
#       __init__.py
#       user.py

# __init__.py uses: re-export symbols, set __version__, control public API
# mypackage/__init__.py
from .models import User, Product
from .services.user import UserService
__version__ = "2.1.0"
__all__ = ["User", "Product", "UserService"]

# Consumer: clean import without knowing internal structure
from mypackage import User   # instead of: from mypackage.models import User

# Import search order (sys.path):
# 1. sys.modules cache
# 2. Built-in modules (sys, os, math)
# 3. Frozen modules
# 4. sys.path directories (cwd, PYTHONPATH, stdlib, site-packages)

📝 Knowledge Check

🧠 Quiz Question 1 of 5

What is the primary purpose of a metaclass in Python?





🧠 Quiz Question 2 of 5

What does asyncio.gather() achieve compared to sequential awaits?





🧠 Quiz Question 3 of 5

What does __slots__ do in a Python class and when should you use it?





🧠 Quiz Question 4 of 5

What does functools.lru_cache require of the function arguments it caches?





🧠 Quiz Question 5 of 5

Why is it unsafe to unpickle data received from an untrusted source?