Toolmingo
Guides22 min read

50 Python Interview Questions (With Answers)

Top Python interview questions with clear answers and code examples — covering data types, OOP, generators, decorators, async, the GIL, and common gotchas.

Python interviews test language fundamentals, OOP design, data structures, and runtime behaviour. This guide covers 50 of the most common questions — with concise answers and runnable code examples.

Quick reference

Topic Most asked questions
Types & mutability Mutable vs immutable, is vs ==, interning
Data structures List vs tuple vs set vs dict, comprehensions
Functions *args/**kwargs, closures, default arg trap
Decorators @functools.wraps, factory decorators, @property
OOP __init__ vs __new__, MRO, dunder methods
Generators yield, lazy evaluation, itertools
Error handling Exception hierarchy, raise ... from, context managers
Concurrency GIL, threading vs multiprocessing vs asyncio
Modules import system, __name__ == "__main__"
Tricky questions Mutable defaults, late binding, None checks

Types and mutability

1. What is the difference between mutable and immutable types?

Immutable objects cannot be changed after creation. Mutable objects can.

Immutable Mutable
int, float, bool list
str dict
tuple set
frozenset bytearray
bytes user-defined classes (usually)
# Immutable — reassignment creates a new object
x = "hello"
id_before = id(x)
x += " world"
print(id(x) == id_before)  # False — new string object

# Mutable — modified in place
lst = [1, 2, 3]
id_before = id(lst)
lst.append(4)
print(id(lst) == id_before)  # True — same object

Mutability matters for dictionary keys (must be hashable/immutable) and function default arguments.


2. What is the difference between is and ==?

  • == checks value equality (calls __eq__)
  • is checks identity — whether both variables point to the same object in memory
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)   # True  — same values
print(a is b)   # False — different objects
print(a is c)   # True  — same object

# None check — always use `is`, not `==`
x = None
print(x is None)  # True  ✓
print(x == None)  # True  but fragile (can be overridden by __eq__)

Use is only for singletons: None, True, False.


3. What is integer interning?

CPython caches small integers (typically −5 to 256) and short strings, so is may return True even for "different" objects.

a = 100
b = 100
print(a is b)  # True  — cached (CPython implementation detail)

a = 1000
b = 1000
print(a is b)  # False — not cached (implementation detail, may vary)

Never rely on is for value comparison of integers or strings.


4. How does Python handle variable scoping (LEGB)?

Python looks up names in this order:

  1. Local — inside the current function
  2. Enclosing — enclosing function scopes (closures)
  3. Global — module-level
  4. Built-in — built-in names (len, print, etc.)
x = "global"

def outer():
    x = "enclosing"
    def inner():
        # x = "local"  # if this line is uncommented, local wins
        print(x)       # "enclosing" — found in enclosing scope
    inner()

outer()

Use global to write to a global variable, nonlocal to write to an enclosing variable.


Data structures

5. What are the differences between list, tuple, set, and dict?

Feature list tuple set dict
Ordered Yes (insertion) Yes No (3.7+ insertion order) Yes (3.7+ insertion order)
Mutable Yes No Yes Yes
Duplicates Yes Yes No Keys: No, Values: Yes
Indexable Yes Yes No By key
Use case General sequence Fixed record, dict key Unique items, set ops Key-value mapping

6. When would you use a tuple instead of a list?

  • When the data should not change (coordinates, RGB colour, DB row)
  • As dictionary keys (lists are unhashable)
  • For named records via collections.namedtuple or typing.NamedTuple
  • Slightly faster than lists for iteration (no overhead for mutability)
from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

p = Point(1.0, 2.0)
print(p.x, p.y)      # 1.0 2.0
print(p[0])           # 1.0 — still indexable
coords = {p: "origin"}  # usable as dict key

7. How does a Python dict work internally?

Python dicts are hash tables — each key is hashed to find a slot.

  • dict[key] is O(1) average for lookup, insert, delete
  • Keys must be hashable (immutable)
  • As of Python 3.7+, insertion order is guaranteed
d = {"a": 1, "b": 2}

# Safe access patterns
value = d.get("c", 0)          # default 0 if missing
d.setdefault("d", []).append(1) # create key if absent

# Merging (Python 3.9+)
merged = d | {"e": 5}

# defaultdict avoids KeyError
from collections import defaultdict
counts = defaultdict(int)
for ch in "hello":
    counts[ch] += 1

8. What is a list comprehension and when should you prefer a generator expression?

# List comprehension — materialises all values in memory
squares = [x**2 for x in range(1000)]

# Generator expression — lazy, one value at a time
squares_gen = (x**2 for x in range(1000))

# Prefer generator when:
# 1. You only iterate once
# 2. The dataset is large
total = sum(x**2 for x in range(1_000_000))  # no list in memory

# Prefer list when:
# 1. You need indexing / len()
# 2. You iterate multiple times

Nested comprehensions are readable up to two levels; beyond that, use explicit loops.


Functions

9. What is the difference between *args and **kwargs?

  • *args captures positional arguments as a tuple
  • **kwargs captures keyword arguments as a dict
def demo(*args, **kwargs):
    print(args)    # (1, 2, 3)
    print(kwargs)  # {'x': 10, 'y': 20}

demo(1, 2, 3, x=10, y=20)

# Unpacking into a function call
def add(a, b, c):
    return a + b + c

nums = [1, 2, 3]
print(add(*nums))         # 6

params = {"a": 1, "b": 2, "c": 3}
print(add(**params))      # 6

10. What is the mutable default argument trap?

Default argument values are evaluated once when the function is defined, not on each call.

# BUG: same list is shared across all calls
def append_to(value, lst=[]):
    lst.append(value)
    return lst

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2]  ← NOT [2]!

# FIX: use None as sentinel
def append_to_fixed(value, lst=None):
    if lst is None:
        lst = []
    lst.append(value)
    return lst

print(append_to_fixed(1))  # [1]
print(append_to_fixed(2))  # [2]  ✓

11. What is a closure in Python?

A closure is a function that remembers the variables from its enclosing scope even after the outer function has returned.

def make_counter(start=0):
    count = start
    def increment(step=1):
        nonlocal count
        count += step
        return count
    return increment

counter = make_counter(10)
print(counter())    # 11
print(counter())    # 12
print(counter(5))   # 17

Closures are the basis for decorators, factories, and partial application.


12. What is the late binding problem in closures?

Variables in closures are looked up at call time, not at definition time.

# BUG: all lambdas see the same `i` = 9 after the loop
funcs = [lambda: i for i in range(10)]
print(funcs[0]())  # 9, not 0!
print(funcs[3]())  # 9, not 3!

# FIX: capture the current value as a default argument
funcs = [lambda i=i: i for i in range(10)]
print(funcs[0]())  # 0 ✓
print(funcs[3]())  # 3 ✓

Decorators

13. What is a decorator?

A decorator is a function that wraps another function to extend its behaviour without modifying it.

import functools
import time

def timer(func):
    @functools.wraps(func)  # preserves __name__, __doc__
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(1_000_000)  # slow_sum took 0.0312s

Without @functools.wraps, slow_sum.__name__ would be "wrapper".


14. How do you write a decorator that accepts arguments?

Add an extra outer function to receive the arguments.

import functools

def retry(times=3, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == times:
                        raise
                    print(f"Attempt {attempt} failed: {e}")
        return wrapper
    return decorator

@retry(times=3, exceptions=(IOError,))
def fetch(url):
    raise IOError("connection refused")

# fetch("http://example.com")  # retries 3 times then raises

15. What does @property do?

@property lets you define a getter (and optionally setter/deleter) for a class attribute using method syntax while providing attribute access syntax.

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 cannot be negative")
        self._radius = value

    @property
    def area(self):
        import math
        return math.pi * self._radius ** 2

c = Circle(5)
print(c.radius)   # 5  — calls getter
c.radius = 10     # calls setter
print(c.area)     # 314.159...
# c.radius = -1   # raises ValueError

Object-oriented programming

16. What is the difference between __init__ and __new__?

  • __new__ creates the instance (class method, called first)
  • __init__ initialises the already-created instance
class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, value):
        self.value = value  # called every time, but same object

a = Singleton(1)
b = Singleton(2)
print(a is b)      # True — same instance
print(a.value)     # 2 — __init__ ran again

Override __new__ for singletons, immutable types (subclassing int, str), or metaclasses.


17. What is the MRO (Method Resolution Order)?

MRO determines the order in which base classes are searched for a method. Python uses the C3 linearisation algorithm.

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

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

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

class D(B, C):
    pass

D().hello()         # B — left-first depth
print(D.__mro__)    # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

Use super() to walk the MRO cooperatively:

class B(A):
    def hello(self):
        super().hello()  # continues to next in MRO
        print("B")

18. What are dunder (magic) methods?

Dunder methods (double underscore) let you define how your objects behave with operators and built-in functions.

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

    def __repr__(self):          # unambiguous string representation
        return f"Vector({self.x}, {self.y})"

    def __str__(self):           # user-friendly string
        return f"({self.x}, {self.y})"

    def __add__(self, other):    # v1 + v2
        return Vector(self.x + other.x, self.y + other.y)

    def __eq__(self, other):     # v1 == v2
        return self.x == other.x and self.y == other.y

    def __len__(self):           # len(v)
        return 2

    def __getitem__(self, idx):  # v[0], v[1]
        return (self.x, self.y)[idx]

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)   # (4, 6)
print(v1 == v2)  # False
print(len(v1))   # 2
print(v1[0])     # 1

19. What is the difference between @staticmethod and @classmethod?

@staticmethod @classmethod
First arg None cls (the class)
Accesses class No Yes
Accesses instance No No
Inheritance Doesn't change Follows subclass
class Date:
    def __init__(self, year, month, day):
        self.year, self.month, self.day = year, month, day

    @classmethod
    def from_string(cls, date_str):
        year, month, day = map(int, date_str.split("-"))
        return cls(year, month, day)   # works correctly in subclasses

    @staticmethod
    def is_valid(date_str):
        parts = date_str.split("-")
        return len(parts) == 3 and all(p.isdigit() for p in parts)

d = Date.from_string("2024-07-14")
print(Date.is_valid("2024-07-14"))  # True

20. What are abstract base classes?

ABCs enforce that subclasses implement specific methods.

from abc import ABC, abstractmethod

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

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

    def describe(self):
        return f"Area={self.area():.2f}, Perimeter={self.perimeter():.2f}"

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

# Shape()   # TypeError: Can't instantiate abstract class
print(Circle(5).describe())  # Area=78.54, Perimeter=31.42

Generators and iterators

21. What is a generator?

A generator is a function that uses yield to produce values lazily — one at a time, on demand — instead of returning them all at once.

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

gen = fibonacci()
print([next(gen) for _ in range(8)])  # [0, 1, 1, 2, 3, 5, 8, 13]

Generators are memory-efficient — they don't build a list in memory.


22. What is the difference between return and yield?

return yield
Returns Once Many times
State Discarded Preserved between calls
Function type Regular Generator
On call Runs immediately Returns generator object
def squares_list(n):
    return [x**2 for x in range(n)]   # builds whole list

def squares_gen(n):
    for x in range(n):
        yield x**2                      # one at a time

# Both iterable, but generator uses O(1) memory
for s in squares_gen(1_000_000):
    if s > 100: break

23. What is yield from?

yield from delegates to a sub-generator, forwarding values and allowing two-way communication.

def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)   # recurse into sublists
        else:
            yield item

data = [1, [2, [3, 4], 5], 6]
print(list(flatten(data)))  # [1, 2, 3, 4, 5, 6]

24. What is the iterator protocol?

Any object implementing __iter__ (returns self) and __next__ (returns next value or raises StopIteration) is an iterator.

class CountUp:
    def __init__(self, start, stop):
        self.current = start
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration
        val = self.current
        self.current += 1
        return val

for n in CountUp(1, 4):
    print(n)   # 1, 2, 3

Error handling

25. What is the Python exception hierarchy?

BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── AttributeError
    ├── ImportError
    │   └── ModuleNotFoundError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── NameError
    ├── TypeError
    ├── ValueError
    ├── OSError
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── TimeoutError
    ├── RuntimeError
    │   └── RecursionError
    └── StopIteration

Catch specific exceptions, not bare except: (which also catches KeyboardInterrupt).


26. What does raise ... from do?

It chains exceptions, preserving the original cause.

def parse_config(text):
    try:
        return int(text)
    except ValueError as e:
        raise RuntimeError(f"Invalid config: {text!r}") from e

try:
    parse_config("abc")
except RuntimeError as e:
    print(e)           # Invalid config: 'abc'
    print(e.__cause__) # invalid literal for int() with base 10: 'abc'

Use raise X from None to suppress the original exception in the traceback.


27. How do context managers work (with statement)?

A context manager implements __enter__ and __exit__. It ensures resources are cleaned up even if an exception occurs.

class ManagedFile:
    def __init__(self, path, mode="r"):
        self.path, self.mode = path, mode

    def __enter__(self):
        self.file = open(self.path, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()
        return False  # don't suppress exceptions

with ManagedFile("data.txt") as f:
    content = f.read()

# Using contextlib — simpler
from contextlib import contextmanager

@contextmanager
def managed_file(path, mode="r"):
    f = open(path, mode)
    try:
        yield f
    finally:
        f.close()

Modules and imports

28. What does if __name__ == "__main__" do?

Python sets __name__ to "__main__" when a script is run directly, and to the module name when it's imported.

# mymodule.py
def greet(name):
    return f"Hello, {name}!"

if __name__ == "__main__":
    # This block runs only when executed directly: python mymodule.py
    print(greet("World"))
    # Not executed when: import mymodule

This pattern lets a file serve as both a reusable module and a standalone script.


29. What is the difference between import X and from X import Y?

import math
print(math.sqrt(9))    # 3.0 — explicit namespace

from math import sqrt
print(sqrt(9))         # 3.0 — direct access

from math import *     # imports all public names — avoid in production

import numpy as np     # alias for long names

from X import Y doesn't save memory — the whole module is still loaded. It just binds the name differently.


30. How does Python's import system work?

  1. Check sys.modules (cache) — if found, return cached module
  2. Find the module: check sys.path directories
  3. Load the module: execute its code in a new namespace
  4. Cache in sys.modules
  5. Bind name in caller's namespace
import sys

# Inspect what's cached
print("json" in sys.modules)   # False initially
import json
print("json" in sys.modules)   # True

# See the search path
print(sys.path[:3])

Concurrency

31. What is the GIL (Global Interpreter Lock)?

The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time.

Implications:

  • CPU-bound tasks: threading gives no speedup — use multiprocessing or ProcessPoolExecutor
  • I/O-bound tasks: threading still helps — the GIL is released during I/O, so threads can interleave
import concurrent.futures

# I/O-bound: threading works well
def fetch(url):
    import urllib.request
    return urllib.request.urlopen(url).read()

urls = ["https://example.com"] * 5
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as ex:
    results = list(ex.map(fetch, urls))  # concurrent HTTP requests

# CPU-bound: use ProcessPoolExecutor
def cpu_task(n):
    return sum(range(n))

with concurrent.futures.ProcessPoolExecutor() as ex:
    results = list(ex.map(cpu_task, [10**6]*4))

32. What is the difference between threading, multiprocessing, and asyncio?

threading multiprocessing asyncio
Parallelism No (GIL) Yes No
Concurrency Yes Yes Yes
Best for I/O-bound CPU-bound I/O-bound (network)
Memory Shared Separate Shared
Overhead Low High Very low
Complexity Medium High Medium
# asyncio — cooperative multitasking for I/O
import asyncio

async def fetch(session, url):
    async with session.get(url) as resp:
        return await resp.text()

async def main():
    import aiohttp
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, f"https://example.com/{i}") for i in range(10)]
        results = await asyncio.gather(*tasks)

asyncio.run(main())

33. How does asyncio work?

asyncio uses an event loop to run coroutines cooperatively. When a coroutine awaits an I/O operation, control returns to the event loop, which runs another coroutine.

import asyncio

async def say_after(delay, message):
    await asyncio.sleep(delay)  # yields control
    print(message)

async def main():
    # Sequential: takes 3 seconds
    # await say_after(1, "one")
    # await say_after(2, "two")

    # Concurrent: takes 2 seconds (overlap)
    await asyncio.gather(
        say_after(1, "one"),
        say_after(2, "two"),
    )

asyncio.run(main())

Memory and performance

34. How does Python manage memory?

  • Reference counting: each object has a count of references; when it reaches 0, memory is freed immediately
  • Cyclic garbage collector: handles reference cycles (A → B → A) that reference counting misses
  • Memory pools: CPython uses pymalloc — pools of fixed-size blocks for small objects (< 512 bytes)
import gc

# Force garbage collection of cycles
gc.collect()

# Disable GC in performance-critical code (if you know you have no cycles)
gc.disable()
# ... performance-critical section ...
gc.enable()

35. What are __slots__?

__slots__ prevents the creation of a __dict__ for each instance, saving memory and speeding up attribute access.

class Point:
    __slots__ = ("x", "y")   # only these attributes allowed

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

p = Point(1, 2)
# p.z = 3  # AttributeError — can't add arbitrary attributes

# Memory comparison (approximate):
# Without __slots__: ~232 bytes per instance
# With __slots__:    ~56 bytes per instance

Use __slots__ when creating millions of instances of a class.


String handling

36. How do you format strings in Python?

name = "Alice"
score = 95.678

# f-strings (Python 3.6+) — recommended
print(f"Hello, {name}! Score: {score:.2f}")

# format() method
print("Hello, {}! Score: {:.2f}".format(name, score))

# % formatting (old style, avoid)
print("Hello, %s! Score: %.2f" % (name, score))

# Multiline f-string
msg = (
    f"Name:  {name}\n"
    f"Score: {score:.1f}\n"
    f"Grade: {'A' if score >= 90 else 'B'}"
)

F-strings support arbitrary expressions: f"{2 + 2}", f"{obj!r}", f"{value:{width}.{precision}f}".


37. How is string immutability relevant in Python?

Strings are immutable — any "modification" creates a new string.

s = "hello"
# s[0] = "H"  # TypeError!

# Concatenation in a loop is O(n²) — each + creates a new string
parts = []
for i in range(1000):
    parts.append(str(i))
result = "".join(parts)   # O(n) — use join!

# str.join is always preferred over repeated +=
words = ["Hello", "World"]
sentence = " ".join(words)  # "Hello World"

Functional programming

38. What are map, filter, and reduce?

from functools import reduce

nums = [1, 2, 3, 4, 5]

# map: apply function to each element
doubled = list(map(lambda x: x * 2, nums))   # [2, 4, 6, 8, 10]
# Prefer: [x * 2 for x in nums]

# filter: keep elements where function returns True
evens = list(filter(lambda x: x % 2 == 0, nums))  # [2, 4]
# Prefer: [x for x in nums if x % 2 == 0]

# reduce: fold list into single value
product = reduce(lambda a, b: a * b, nums)    # 120
# Prefer: math.prod(nums) in Python 3.8+

In Python, list comprehensions and generator expressions are usually preferred over map/filter for clarity.


39. What is functools.partial?

partial creates a new function with some arguments pre-filled.

from functools import partial

def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
cube   = partial(power, exp=3)

print(square(4))  # 16
print(cube(3))    # 27

# Useful with map
nums = [1, 2, 3, 4]
squares = list(map(square, nums))  # [1, 4, 9, 16]

Common patterns and gotchas

40. What are Python's truthy and falsy values?

Falsy: False, 0, 0.0, "", [], {}, set(), None, objects with __bool__ returning False or __len__ returning 0.

Everything else is truthy.

# Pythonic checks
if not my_list:         # empty list
    print("empty")

if user:                # None or falsy user
    do_something(user)

# TRAP: 0 is falsy — check explicitly if 0 is valid
count = 0
if count is None:       # ✓ explicit None check
    count = default

# TRAP: empty string vs missing value
if value is not None:   # ✓ distinguishes "" from None
    process(value)

41. How does Python's copy vs deepcopy work?

import copy

original = {"a": [1, 2, 3], "b": [4, 5]}

# Shallow copy — new dict, but inner lists are shared
shallow = copy.copy(original)        # or: original.copy() / dict(original)
shallow["a"].append(99)
print(original["a"])   # [1, 2, 3, 99] — modified!

# Deep copy — completely independent
original = {"a": [1, 2, 3], "b": [4, 5]}
deep = copy.deepcopy(original)
deep["a"].append(99)
print(original["a"])   # [1, 2, 3] — unchanged ✓

42. What is the difference between sorted() and .sort()?

sorted() .sort()
Works on Any iterable Lists only
Returns New list None
Original Unchanged Modified in place
nums = [3, 1, 4, 1, 5]

# sorted — non-destructive
ascending = sorted(nums)           # [1, 1, 3, 4, 5]
descending = sorted(nums, reverse=True)

# .sort() — in place
nums.sort(key=lambda x: -x)       # descending, modifies nums

# Sort by multiple keys
people = [("Alice", 30), ("Bob", 25), ("Carol", 30)]
people.sort(key=lambda p: (p[1], p[0]))  # age then name

43. What is a decorator factory (parameterised decorator)?

import functools

def validate_types(**expected_types):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for name, expected in expected_types.items():
                if name in kwargs and not isinstance(kwargs[name], expected):
                    raise TypeError(f"{name} must be {expected.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(name=str, age=int)
def create_user(name, age):
    return {"name": name, "age": age}

print(create_user(name="Alice", age=30))
# create_user(name="Alice", age="thirty")  # TypeError

44. How do you implement a Singleton in Python?

# Method 1: using __new__
class Singleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# Method 2: using a module (most Pythonic)
# config.py — just import it, modules are loaded once
# import config; config.value = 42

# Method 3: using a metaclass
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("postgres://localhost/mydb")
db2 = Database("postgres://localhost/other")
print(db1 is db2)   # True
print(db1.url)      # "postgres://localhost/mydb"

45. What is collections.Counter?

Counter is a dict subclass for counting hashable objects.

from collections import Counter

text = "the quick brown fox jumps over the lazy dog"
word_counts = Counter(text.split())
print(word_counts.most_common(3))   # [('the', 2), ('quick', 1), ...]

# Arithmetic on counters
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2)   # Counter({'a': 4, 'b': 3})
print(c1 - c2)   # Counter({'a': 2}) — negatives dropped

# Count characters
char_count = Counter("banana")   # Counter({'a': 3, 'n': 2, 'b': 1})

Typing and dataclasses

46. What are Python type hints?

Type hints (PEP 484) let you annotate types for static analysis tools like mypy. They are not enforced at runtime by default.

from typing import Optional, Union, List, Dict

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

# Modern syntax (Python 3.10+)
def find(items: list[int], target: int) -> int | None:
    for i, v in enumerate(items):
        if v == target:
            return i
    return None

# TypedDict for structured dicts
from typing import TypedDict

class User(TypedDict):
    name: str
    age: int
    email: str | None

47. What is a dataclass?

@dataclass auto-generates __init__, __repr__, __eq__ from field declarations.

from dataclasses import dataclass, field
from typing import List

@dataclass(order=True, frozen=False)
class Employee:
    name: str
    department: str
    salary: float = 50_000.0
    skills: List[str] = field(default_factory=list)

    def give_raise(self, amount: float) -> None:
        self.salary += amount

emp = Employee("Alice", "Engineering", 80_000)
emp.give_raise(10_000)
print(emp)  # Employee(name='Alice', department='Engineering', salary=90000.0, skills=[])

# frozen=True makes it immutable (hashable)
@dataclass(frozen=True)
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
# p.x = 3.0  # FrozenInstanceError

Common interview traps

48. What will this code print?

def foo(x, lst=[]):
    lst.append(x)
    return lst

print(foo(1))   # [1]
print(foo(2))   # [1, 2]  ← NOT [2]
print(foo(3))   # [1, 2, 3]

Why: The default lst=[] is created once. All calls share the same list object.


49. What is the output of this code?

class A:
    x = []

a = A()
b = A()
a.x.append(1)
print(b.x)       # [1]  — class attribute shared

a.x = [99]       # now a.x is an INSTANCE attribute
print(b.x)       # [1]  — b.x still refers to class attribute
print(A.x)       # [1]  — unchanged class attribute

Mutation via a.x.append() modifies the shared class attribute. Assignment (a.x = ...) creates a new instance attribute, shadowing the class attribute for that instance.


50. How does Python's sorted handle complex objects?

from dataclasses import dataclass

@dataclass
class Student:
    name: str
    grade: float
    age: int

students = [
    Student("Alice", 3.8, 22),
    Student("Bob", 3.8, 21),
    Student("Carol", 3.5, 23),
]

# Sort by grade descending, then name ascending
ranked = sorted(students, key=lambda s: (-s.grade, s.name))
for s in ranked:
    print(f"{s.name}: {s.grade}")
# Alice: 3.8
# Bob:   3.8
# Carol: 3.5

# Using attrgetter — faster than lambda for simple attributes
from operator import attrgetter
ranked = sorted(students, key=attrgetter("grade", "name"))

Common mistakes

Mistake Wrong Right
Mutable default def f(lst=[]) def f(lst=None)
Late binding lambda: i in loop lambda i=i: i
is for values if x is 1000 if x == 1000
None check if x == None if x is None
Modify while iterate for i in lst: lst.remove(i) Iterate copy or use list comprehension
Catching bare except except: except Exception: or specific type
Ignoring StopIteration Use for loops or next(gen, default)
String concatenation in loop s += x in loop "".join(parts)

FAQ

Q: Is Python pass-by-value or pass-by-reference?
A: Neither exactly. Python uses pass-by-object-reference (pass-by-assignment). You pass a reference to the object. Immutable objects can't be changed; mutable objects can be mutated but not rebound in the caller.

Q: What is the difference between deepcopy and pickle?
A: Both create independent copies, but pickle serialises to bytes (for file storage/network), while deepcopy creates an in-memory copy. pickle handles more types but is slower.

Q: When should I use asyncio vs threading?
A: Use asyncio for network I/O with many concurrent connections (web scraping, APIs). Use threading for blocking I/O (legacy libs without async support). Use multiprocessing for CPU-bound work.

Q: What is the difference between __str__ and __repr__?
A: __repr__ should be unambiguous and ideally valid Python to recreate the object. __str__ should be human-readable. print() uses __str__; the REPL and repr() use __repr__. If only __repr__ is defined, it's used for both.

Q: How do you avoid circular imports?
A: Restructure code to remove the circular dependency (move shared code to a third module), use local imports inside functions, or import the module rather than the name (import module instead of from module import name).

Q: What is __all__ in a module?
A: __all__ is a list of names that are exported when a user does from module import *. It also signals the public API. Names not in __all__ are still importable directly — it only affects import *.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools