Menu
CoddyTech

Python Interview Questions and Answers

Python questions from fresher basics to senior topics, each with a short answer you can say out loud. Runnable examples were run on Python 3.11, and coding questions link to problems on Coddy's judge.

88 questions19 output quizzesRunnable code checked on Python 3.11By Kevin Spektor, Co-founder & CTO

Python interview questions for freshers

Data types, mutability, slicing, functions, scope and exceptions, as asked in campus and junior interviews.

What is Python and what are its key features?

Fresherbasics

Python is a high-level, general-purpose language: dynamically but strongly typed, garbage collected, with a large standard library. It is used for scripting, web backends, automation and data work. See what Python is.

Follow-up: Is Python compiled or interpreted? Both: CPython compiles source to bytecode, then interprets the bytecode.

What is PEP 8?

Fresherbasics

PEP 8 is Python's official style guide: 4 spaces per indent, snake_case for functions and variables, CapWords for classes, UPPER_CASE for constants, lines up to 79 characters, imports grouped at the top. Teams enforce it with a linter such as ruff and a formatter such as black.

What is the difference between a list and a tuple in Python?

Fresherdata types

A list is mutable and a tuple is immutable, so a tuple is hashable when its items are and can be a dict key or a set member; a list cannot.

Python
point = (3, 4)
labels = {point: "A"}
print(labels[(3, 4)])

try:
    labels[[3, 4]] = "B"
except TypeError as e:
    print("TypeError:", e)

The trap: a list inside a tuple can still change, so t = ([1], 2); t[0].append(5) works. See tuples.

What is the difference between == and is in Python?

Fresherdata types

== compares values (it calls __eq__); is checks whether both names point to the same object.

Python
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)
print(a is b)
c = a
print(c is a)
x = None
print(x is None)

a and b are equal but separate lists. Use is only for singletons such as None, because a class can override __eq__ but not is.

What does this print: += on a string that has a second name?

Fresherdata types
Python
s = "hello"
t = s
s += " world"
print(t, s)

Predict the output

Strings are immutable, so s += " world" builds a new string and rebinds s; t still points to "hello". With a list the result flips, because += extends a list in place and t sees [1, 2]:

Python
s = [1]
t = s
s += [2]
print(t)

Is Python dynamically typed or statically typed? Is it strongly typed?

Fresherdata types

Python is dynamically typed (types belong to values, and a name can be rebound to any type) and strongly typed (it never silently converts between unrelated types).

Python
x = 5
print(type(x).__name__)
x = "five"
print(type(x).__name__)

try:
    print("3" + 3)
except TypeError as e:
    print("TypeError:", e)

In JavaScript, which is weakly typed, "3" + 3 gives "33".

What is a list comprehension in Python?

Freshersyntax

A list comprehension builds a list in one expression, [expression for item in iterable if condition], shorter than a loop with append and usually a little faster.

Python
nums = [1, 2, 3, 4, 5, 6]
squares_of_evens = [n * n for n in nums if n % 2 == 0]
print(squares_of_evens)

words = ["apple", "kiwi", "banana"]
lengths = {w: len(w) for w in words}
print(lengths)

first_letters = {w[0] for w in words}
print(sorted(first_letters))

The same syntax builds dicts and sets; with parentheses you get a lazy generator expression. More in list comprehensions.

What does this print: slices with a step, a reverse and a negative start?

Freshersyntax
Python
nums = [0, 1, 2, 3, 4, 5]
print(nums[1:5:2], nums[::-1][:2], nums[-2:])

Predict the output

A slice is start:stop:step with stop excluded, so nums[1:5:2] takes indexes 1 and 3. nums[::-1] is a reversed copy starting 5, 4, and nums[-2:] is the last two items. Slicing never raises IndexError and always returns a new list.

What are *args and **kwargs in Python?

Fresherfunctions

*args collects extra positional arguments into a tuple and **kwargs collects extra keyword arguments into a dict. The stars do the work; the names are a convention.

Python
def describe(name, *args, **kwargs):
    print(name, args, kwargs)

describe("order", 1, 2, size="L", paid=True)

nums = [10, 20]
opts = {"size": "M"}
describe("unpacked", *nums, **opts)

At a call site the stars unpack, which is how a decorator forwards everything with func(*args, **kwargs). See args and kwargs.

What is the difference between break, continue and pass?

Freshercontrol flow

break exits the loop, continue skips to the next iteration, and pass does nothing (a placeholder where Python needs a statement).

Python
for n in range(6):
    if n == 1:
        pass          # placeholder, the loop carries on
    if n == 2:
        continue      # skip the print for 2
    if n == 4:
        break         # stop the loop at 4
    print(n)

for n in [1, 3, 5]:
    if n % 2 == 0:
        print("found an even number")
        break
else:
    print("no even number")

The second loop shows a loop's else, which runs only when the loop ends without break.

What does this print: assigning to a global's name inside a function?

Fresherscope
Python
x = 10

def change():
    x = 20
    return x

print(change(), x)

Predict the output

Assigning to x inside a function creates a local variable, so the global stays 10; rebinding the global needs global x. The trap: x = x + 1 in a function raises UnboundLocalError, because the assignment makes x local for the whole body.

What does this print: pass by value or by reference?

Fresherfunctions
Python
def modify(items, count):
    items.append(4)
    count += 1
    items = [0]

nums = [1, 2, 3]
total = 10
modify(nums, total)
print(nums, total)

Predict the output

Python passes object references by value, often called call by sharing: mutating the object is visible to the caller, rebinding the parameter is not. items.append(4) changes nums, while count += 1 and items = [0] only rebind local names, so total stays 10 and nums never becomes [0].

What does this print: range with a negative step?

Freshercontrol flow
Python
print(list(range(10, 0, -3)))

Predict the output

range(start, stop, step) never includes stop; with a negative step it counts down while the value is above stop: 10, 7, 4, 1. In Python 3 range is lazy, not a list, but it supports len(), indexing and a constant-time in for integers.

How does exception handling work in Python (try, except, else, finally)?

Fresherexceptions

try holds code that may fail, except handles a specific exception, else runs only when nothing was raised, and finally always runs, even after a return.

Python
def parse(text):
    try:
        value = int(text)
    except ValueError:
        print("bad input:", text)
        return None
    else:
        print("parsed", value)
        return value
    finally:
        print("finally runs for", text)

parse("42")
parse("4x")

Catch the narrowest exception you can; a bare except: also catches KeyboardInterrupt and hides bugs. More in exceptions.

What is a lambda function in Python?

Fresherfunctions

A lambda is an anonymous single-expression function, such as lambda x: x * 2, used mostly as a key or a callback.

Python
people = [("Asha", 31), ("Ravi", 25), ("Meera", 28)]
by_age = sorted(people, key=lambda p: p[1])
print(by_age)

double = lambda x: x * 2
print(list(map(double, [1, 2, 3])))

It cannot hold statements. If you give one a name, as with double, PEP 8 says to write a def.

What does if __name__ == "__main__": do?

Freshermodules

A file run directly has __name__ == "__main__"; an imported one has its module name. The guard runs code only when the file is executed as a script, so importing it for one function does not also start its server or tests.

Python
def main():
    print("running as a script")

print("__name__ is", __name__)

if __name__ == "__main__":
    main()

See the main function.

What does this print: 0.1 + 0.2 compared with 0.3?

Freshernumbers
Python
print(0.1 + 0.2 == 0.3, round(0.1 + 0.2, 2) == 0.3)

Predict the output

0.1, 0.2 and 0.3 have no exact binary representation, so 0.1 + 0.2 is not exactly 0.3; rounding to two places hides the error.

Python
import math
from decimal import Decimal

print(0.1 + 0.2)
print(math.isclose(0.1 + 0.2, 0.3))
print(Decimal("0.1") + Decimal("0.2"))

Compare floats with math.isclose, and handle money with decimal.Decimal or integer cents.

What does this print: // and % with negative numbers?

Freshernumbers
Python
print(7 // 2, -7 // 2, 7 % -3)

Predict the output

// rounds toward negative infinity, not toward zero, so -7 // 2 is -4 (C and Java give -3). % follows it, so its result takes the divisor's sign: 7 % -3 is -2.

What are f-strings and how do you format numbers with them?

Fresherstrings

An f-string (f"...", Python 3.6+) evaluates the expressions in {} and formats them in place; it is the most readable formatting style.

Python
name = "Priya"
score = 91.456
items = 3
print(f"{name} scored {score:.1f}")
print(f"{items:03d} | {name:>8} | {score:,.2f}")
print(f"{items * 2 = }")

After the colon comes the format spec, such as .1f, 03d, >8 or ,. The = form (3.8+) prints the expression and its value.

OOPs in Python interview questions

Classes, inheritance and the dunder methods that make Python objects behave like built-ins.

What is a class and what is an object in Python?

Fresheroop

A class is a blueprint of attributes and methods; an object is one instance built from it, set up by __init__.

Python
class BankAccount:
    bank = "Coddy Bank"              # class attribute, shared

    def __init__(self, owner, balance=0):
        self.owner = owner           # instance attributes
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        return self.balance

a = BankAccount("Ravi")
b = BankAccount("Asha", 500)
a.deposit(100)
print(a.owner, a.balance, b.owner, b.balance, a.bank)

Each instance has its own owner and balance, while the class attribute bank is shared. See classes.

What is self in Python?

Fresheroop

self is the instance a method was called on, passed automatically as the first argument, so a.deposit(100) is BankAccount.deposit(a, 100). It is a convention, not a keyword, and there is no implicit this, so attributes need self.balance.

How do you make a variable private in Python?

Fresheroopencapsulation

Python has no private keyword. _balance marks an attribute as internal by convention; __balance is name-mangled to _ClassName__balance, so subclasses do not overwrite it by accident.

Python
class Account:
    def __init__(self):
        self._internal = "by convention"
        self.__secret = 42

acc = Account()
print(acc._internal)
try:
    print(acc.__secret)
except AttributeError as e:
    print("AttributeError:", e)
print(acc._Account__secret)

Mangling prevents clashes, not access. For controlled access, use a @property.

What is polymorphism in Python, and what is duck typing?

Fresherooppolymorphism

Polymorphism means one interface with many implementations. In Python it mostly comes from duck typing: if an object has the method you call, it works, whatever its class.

Python
class Dog:
    def speak(self):
        return "Woof"

class Robot:
    def speak(self):
        return "Beep"

for thing in [Dog(), Robot()]:
    print(thing.speak())

print(len("abc"), len([1, 2]), len({"a": 1}))

Python has no overloading by signature; a second def with the same name replaces the first, so use default arguments or functools.singledispatch.

What is the difference between __new__ and __init__?

Senioroop

__new__ creates and returns the object; __init__ then initializes it and must return None.

Python
class Point:
    def __new__(cls, *args):
        print("__new__ called")
        return super().__new__(cls)

    def __init__(self, x, y):
        print("__init__ called")
        self.x, self.y = x, y

p = Point(1, 2)
print(p.x, p.y)

You override __new__ mainly to subclass immutable types like int or tuple, or for singletons. If it returns something that is not an instance of the class, __init__ is skipped.

What does this print: a list defined in the class body?

Experiencedoop
Python
class Basket:
    items = []

    def add(self, x):
        self.items.append(x)

a = Basket()
b = Basket()
a.add(1)
b.add(2)
print(a.items)

Predict the output

items = [] in the class body is one list shared by every instance, and self.items.append(x) mutates it, so both objects see both values. Create the list in __init__ with self.items = []. An assignment to self.items would create an instance attribute; only in-place mutation leaks.

What is the difference between @staticmethod and @classmethod?

Experiencedoop

A class method receives the class (cls), so it can build instances and read class state; a static method receives nothing implicit and is a plain function in the class namespace.

Python
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)

    @staticmethod
    def is_leap(year):
        return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

d = Date.from_string("2024-02-29")
print(d.year, d.month, d.day, Date.is_leap(2024))

With cls, SubDate.from_string(...) returns a SubDate; a static method that hardcodes Date(...) could not.

What is the difference between __str__ and __repr__?

Experiencedoopdunder methods

__str__ is the readable form used by print() and str(); __repr__ is the unambiguous developer form used by the REPL, repr() and containers.

Python
class User:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"User(name={self.name!r})"

    def __str__(self):
        return self.name

u = User("Asha")
print(u)
print(repr(u))
print([u])

A list prints the repr of its items, as the last line shows. If you implement only one, make it __repr__, because str() falls back to it.

How do you create an abstract class or interface in Python?

Experiencedoopabstraction

Inherit from abc.ABC and mark the required methods with @abstractmethod; a class with unimplemented abstract methods cannot be instantiated.

Python
from abc import ABC, abstractmethod

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

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

print(Square(3).area())
try:
    Shape()
except TypeError as e:
    print("TypeError:", e)

Python has no interface keyword, so an ABC with only abstract methods plays that role. typing.Protocol gives duck-typed interfaces without inheritance.

What does the @property decorator do?

Experiencedoopencapsulation

@property turns a method into an attribute read without parentheses, and its .setter runs on assignment, usually to validate. It replaces get_x() and set_x() methods.

Python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("below absolute zero")
        self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32

t = Temperature(25)
print(t.fahrenheit)
try:
    t.celsius = -300
except ValueError as e:
    print("ValueError:", e)

__init__ assigns through the setter, so the validation also applies on creation.

What does this print: super() in a diamond hierarchy?

Senioroopinheritance
Python
class A:
    def hi(self):
        return "A"

class B(A):
    def hi(self):
        return "B" + super().hi()

class C(A):
    def hi(self):
        return "C" + super().hi()

class D(B, C):
    def hi(self):
        return "D" + super().hi()

print(D().hi())

Predict the output

Python resolves methods along the MRO, computed with C3 linearization; here it is D, B, C, A, object (check D.__mro__). super() means the next class in the MRO of the object's actual type, not the parent, so super() inside B calls C.hi and A runs once. See inheritance.

What are dataclasses in Python and when would you use one?

Experiencedoop

@dataclass (Python 3.7+) generates __init__, __repr__ and __eq__ from the annotated fields. frozen=True makes instances immutable and hashable, and order=True adds comparisons.

Python
from dataclasses import dataclass, field

@dataclass(frozen=True)
class Point:
    x: int
    y: int

@dataclass
class Cart:
    owner: str
    items: list = field(default_factory=list)

p = Point(1, 2)
print(p, p == Point(1, 2))
c = Cart("Ravi")
c.items.append("book")
print(c)

Mutable defaults need field(default_factory=list); items: list = [] raises ValueError when the class is defined, which guards against the mutable default argument trap.

Python data structures interview questions

Lists, dicts, sets and the standard library containers, with the time complexity you are expected to know.

How do you iterate over a dictionary in Python?

Fresherdict

Looping over a dict gives its keys; .items() gives key and value pairs and .values() the values, in insertion order.

Python
stock = {"apple": 3, "banana": 0, "kiwi": 7}

for key in stock:
    print(key, end=" ")
print()

for key, qty in stock.items():
    if qty == 0:
        print("out of stock:", key)

print(stock.get("mango", 0))
print({k: v for k, v in stock.items() if v > 0})

Adding or removing keys while iterating raises RuntimeError, so iterate over list(d) instead. See dictionaries.

What does this print: append vs extend with a list?

Fresherlists
Python
a = [1, 2]
b = [1, 2]
a.append([3, 4])
b.extend([3, 4])
print(a, len(a), b, len(b))

Predict the output

append adds its argument as one item, even a list; extend adds each item of an iterable, so a has length 3 and b length 4. Both return None, so a = a.append(x) sets a to None.

What is a set in Python and when do you use one?

Fresherset

A set holds unique, hashable items with O(1) average membership tests. Use it to remove duplicates, test membership and do set algebra.

Python
a = {1, 2, 3, 4}
b = {3, 4, 5}
print(a | b, a & b, a - b, a ^ b)

nums = [3, 1, 3, 2, 1]
print(len(nums) != len(set(nums)))
print(4 in a)

Two traps: {} is an empty dict (use set()), and a set cannot hold lists.

How do you implement a stack in Python?

Fresherstack

Use a list: append pushes and pop() pops, both O(1), and stack[-1] is the top.

Python
def balanced(text):
    pairs = {")": "(", "]": "[", "}": "{"}
    stack = []
    for ch in text:
        if ch in "([{":
            stack.append(ch)
        elif ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
    return not stack

print(balanced("{[()]}"), balanced("([)]"), balanced("(("))

Bracket matching is the classic stack question. Do not use a list as a queue, because pop(0) is O(n). See the stack visualization.

What does this print: changing a shallow copy of nested lists?

Experiencedcopying
Python
import copy

a = [[1, 2], [3]]
b = copy.copy(a)
b[0].append(99)
b.append([4])
print(a)

Predict the output

copy.copy makes a new outer list holding the same inner lists, so b[0].append(99) shows up in a while b.append([4]) does not. copy.deepcopy copies recursively. a[:] and a.copy() are shallow too, which is fine for immutable items.

What does this print: a 2D grid built with *?

Experiencedcopying
Python
grid = [[0] * 3] * 2
grid[0][0] = 1
print(grid)

Predict the output

[row] * 2 repeats the reference, so both rows are one list and a write through one shows in the other. Build a grid with a comprehension, [[0] * 3 for _ in range(2)]; [0] * 3 alone is safe because integers are immutable.

Which classes from the collections module do you use, and when?

Experiencedcollections

The four you use most are Counter for counting, defaultdict for grouping, deque for queues and sliding windows, and namedtuple for small immutable records.

Python
from collections import Counter, defaultdict, deque, namedtuple

words = "the cat and the hat and the bat".split()
print(Counter(words).most_common(2))

by_letter = defaultdict(list)
for w in words:
    by_letter[w[0]].append(w)
print(dict(by_letter))

window = deque([1, 2, 3], maxlen=3)
window.append(4)
print(window)

Point = namedtuple("Point", "x y")
print(Point(2, 5).y)

Counter.most_common(k) answers "top k" questions in one line, and deque(maxlen=n) drops the oldest item automatically.

What is the time complexity of common list, dict and set operations?

Seniorcomplexity

A list is a dynamic array: indexing, append and pop() are O(1), while in, insert(0, x) and pop(0) are O(n), and sort() is O(n log n). Dict and set are hash tables with O(1) average lookup, insert and delete.

Follow-up: How do you speed up if x in big_list inside a loop? Convert big_list to a set first, which turns O(n * m) into O(n + m).

Why use collections.deque instead of a list for a queue?

Experiencedcollectionsqueue

list.pop(0) is O(n) because every remaining element shifts left; deque.popleft() is O(1).

Python
from collections import deque

queue = deque()
for job in ["a", "b", "c"]:
    queue.append(job)
while queue:
    print("processing", queue.popleft())

d = deque([1, 2, 3, 4])
d.rotate(1)
print(d)

Indexing into the middle of a deque is O(n), so it is not a drop-in list replacement. Breadth-first search in Python should use a deque; between threads, use queue.Queue.

How do you use a heap or priority queue in Python?

Experiencedheap

heapq turns a list into a binary min-heap: heappush and heappop are O(log n) and heap[0] is the smallest item. Up to 3.13 there is no max-heap API (3.14 added heappush_max), so push negated values.

Python
import heapq

nums = [5, 1, 8, 3, 9, 2]
heap = []
for n in nums:
    heapq.heappush(heap, n)
print(heap[0], heapq.heappop(heap), heap[0])

print(heapq.nlargest(3, nums))

k, top = 3, []
for n in nums:
    heapq.heappush(top, n)
    if len(top) > k:
        heapq.heappop(top)
print(top[0])

The last block is the "kth largest" answer: a min-heap of size k, O(n log k). See the heap visualization.

How does a Python dictionary work internally?

Seniordictinternals

A dict is a hash table: the key's hash picks a slot and collisions probe other slots, so lookup, insert and delete are O(1) on average and O(n) when many keys collide. Since CPython 3.6 entries sit in a dense array in insertion order, which is why dicts keep order (guaranteed from 3.7, see the dict docs). See the hash table visualization.

How does sorting work in Python, and what does it mean that it is stable?

Seniorsorting

sorted() returns a new list; list.sort() sorts in place and returns None. Both use Timsort (with powersort merging since 3.11): O(n log n), near O(n) on partly sorted data, and stable, so equal items keep their input order.

Python
students = [("Ravi", "B", 82), ("Asha", "A", 91), ("Meera", "B", 91), ("Kiran", "A", 75)]

by_score = sorted(students, key=lambda s: s[2], reverse=True)
print([s[0] for s in by_score])

by_section_then_score = sorted(students, key=lambda s: (s[1], -s[2]))
print([s[0] for s in by_section_then_score])

nums = [3, 1, 2]
print(nums.sort(), nums)

Asha stays ahead of Meera at 91 because she came first. See the merge sort page.

Python coding interview questions

Short programs from coding rounds, each with the idiomatic Python answer and a judge problem to practice.

How do you reverse a string in Python?

Fresherstringscoding

Slice with a step of -1: s[::-1].

Python
s = "coddy"
print(s[::-1])
print("".join(reversed(s)))

def is_palindrome(text):
    cleaned = [c.lower() for c in text if c.isalnum()]
    return cleaned == cleaned[::-1]

print(is_palindrome("A man, a plan, a canal: Panama"))

If slicing is banned, swap characters in a list with two pointers. Avoid building the result with += in a loop, which can be O(n squared).

How do you find two numbers in a list that add up to a target?

Fresherhashingcoding

Walk the list once, keeping a dict from value to index, and check whether target - n was already seen: O(n) time instead of O(n squared).

Python
def two_sum(nums, target):
    seen = {}
    for i, n in enumerate(nums):
        if target - n in seen:
            return [seen[target - n], i]
        seen[n] = i
    return None

print(two_sum([2, 7, 11, 15], 9))
print(two_sum([3, 2, 4], 6))
print(two_sum([1, 2], 7))

Check before inserting, so a number is never paired with itself.

How do you check if two strings are anagrams in Python?

Fresherhashingcoding

Compare letter counts with collections.Counter, which is O(n); sorted(a) == sorted(b) is shorter but O(n log n).

Python
from collections import Counter

def is_anagram(a, b):
    return Counter(a) == Counter(b)

print(is_anagram("listen", "silent"), is_anagram("rat", "car"))
print(sorted("listen") == sorted("silent"))

groups = {}
for w in ["eat", "tea", "tan", "ate", "nat", "bat"]:
    groups.setdefault("".join(sorted(w)), []).append(w)
print(list(groups.values()))

The usual follow-up, grouping words into anagram groups, is at the end: anagrams share the same sorted letters, so that string works as a dict key.

Write FizzBuzz in Python.

Freshercoding

Print 1 to n, with "Fizz" for multiples of 3, "Buzz" for multiples of 5 and "FizzBuzz" for both. Building the string piece by piece avoids a separate i % 15 check.

Python
for i in range(1, 16):
    out = ""
    if i % 3 == 0:
        out += "Fizz"
    if i % 5 == 0:
        out += "Buzz"
    print(out or i, end=" ")
print()

out or i prints the number when out is empty, because an empty string is falsy.

How do you generate Fibonacci numbers in Python?

Fresherrecursioncoding

Use a loop with two variables: O(n) time, O(1) space.

Python
from functools import lru_cache

def fib_iter(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

@lru_cache(maxsize=None)
def fib_memo(n):
    return n if n < 2 else fib_memo(n - 1) + fib_memo(n - 2)

print([fib_iter(i) for i in range(10)])
print(fib_memo(80))

Plain recursion takes exponential time; lru_cache makes it O(n) but still hits CPython's default recursion limit of 1000 for large n.

How do you check whether a number is prime in Python?

Freshercoding

Try divisors from 2 while i * i <= n; if none divides n, it is prime. That is O(sqrt n).

Python
def is_prime(n):
    if n < 2:
        return False
    i = 2
    while i * i <= n:
        if n % i == 0:
            return False
        i += 1
    return True

print([n for n in range(30) if is_prime(n)])

Handle 0, 1 and negative numbers first, which is where most bugs are. For all primes up to n, use the Sieve of Eratosthenes.

How do you find the second largest number in a list?

Freshercoding

Keep two variables, first and second, in one pass: O(n) time and O(1) space.

Python
def second_largest(nums):
    first = second = None
    for n in nums:
        if first is None or n > first:
            first, second = n, first
        elif n != first and (second is None or n > second):
            second = n
    return second

print(second_largest([10, 5, 10, 8]))
print(second_largest([3, 3]))

The trap is duplicates: in [10, 5, 10, 8] the answer is 8, and [3, 3] has no second largest. sorted(set(nums))[-2] is O(n log n) and fails on fewer than two distinct values.

How do you remove duplicates from a list while keeping the order?

Freshercoding

Use list(dict.fromkeys(items)): dict keys are unique and keep insertion order.

Python
items = [3, 1, 3, 2, 1, 5]
print(list(dict.fromkeys(items)))
print(list(set(items)))

seen, result = set(), []
for x in items:
    if x not in seen:
        seen.add(x)
        result.append(x)
print(result)

list(set(items)) also removes duplicates but promises no order; it only looks sorted here because small integers hash to themselves. Write the loop when you need a condition or the items are not hashable.

How do you find the first non-repeating character in a string?

Experiencedhashingcoding

Count every character with Counter, then scan the string again and return the first one whose count is 1. Two passes, O(n) time.

Python
from collections import Counter

def first_unique(s):
    counts = Counter(s)
    for i, ch in enumerate(s):
        if counts[ch] == 1:
            return i
    return -1

print(first_unique("swiss"), first_unique("aabbcdc"), first_unique("aabb"))

The second pass must walk the string, not the counter, because the string's order defines "first".

How do you flatten a nested list in Python?

Experiencedrecursioncoding

Recurse: yield each item, and flatten any item that is a list with yield from.

Python
def flatten(items):
    for item in items:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

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

For one level, [x for sub in items for x in sub] is enough. Very deep nesting needs an explicit stack to avoid the recursion limit. See the recursion visualization.

How do you find the longest substring without repeating characters?

Experiencedsliding windowcoding

Use a sliding window and remember each character's last index; when a character repeats inside the window, move the left edge past its previous position. One pass, O(n).

Python
def longest_unique(s):
    last_seen = {}
    start = best = 0
    for i, ch in enumerate(s):
        if ch in last_seen and last_seen[ch] >= start:
            start = last_seen[ch] + 1
        last_seen[ch] = i
        best = max(best, i - start + 1)
    return best

print(longest_unique("abcabcbb"), longest_unique("bbbbb"), longest_unique("pwwkew"))

last_seen[ch] >= start keeps a repeat outside the window from moving start backwards.

How do you implement binary search in Python, and what does bisect do?

Experiencedsearchingcoding

Keep lo and hi bounds on a sorted list and halve the range each step, O(log n); bisect does the same search and returns the insertion point.

Python
import bisect

def binary_search(nums, target):
    lo, hi = 0, len(nums) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if nums[mid] == target:
            return mid
        if nums[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1

nums = [1, 3, 5, 7, 9, 11]
print(binary_search(nums, 7), binary_search(nums, 4))
print(bisect.bisect_left(nums, 4), bisect.bisect_left(nums, 7))
bisect.insort(nums, 4)
print(nums)

Python integers never overflow, so (lo + hi) // 2 is safe. See the binary search visualization.

Python interview questions for experienced developers

For 2 to 10 years of experience: the traps behind real bugs, decorators, generators, context managers and memory.

What does this print: a default list argument used twice?

Experiencedfunctions
Python
def add(item, bucket=[]):
    bucket.append(item)
    return bucket

add(1)
print(add(2))

Predict the output

Default values are evaluated once, when def runs, so every call shares the same list and the 1 from the first call is still there. Use a None sentinel:

def add(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

The bug only appears on the second call, so a quick manual test passes.

What does this print: lambdas created in a loop?

Seniorclosures
Python
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])

Predict the output

Closures capture variables, not values: every lambda reads the same i when it is called, after the loop left i at 2. Bind the current value at creation with a default argument (lambda i=i: i) or functools.partial. Callbacks created in a loop have the same bug.

What is a decorator in Python and how do you write one?

Experiencedfunctionsdecorators

A decorator takes a function and returns a replacement, usually a wrapper that adds behavior around the call; @log_calls above def add means add = log_calls(add).

Python
import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}{args}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(2, 3)
print(add.__name__)

Without functools.wraps, add.__name__ would be wrapper. See decorators.

What is the difference between an iterator and a generator in Python?

Experiencedgenerators

An iterator is any object with __iter__ and __next__. A generator is the easy way to write one: a function with yield that pauses there and resumes on the next next().

Python
def countdown(n):
    print("start")
    while n > 0:
        yield n
        n -= 1
    print("done")

gen = countdown(3)
print(next(gen))
print(list(gen))

Calling countdown(3) runs nothing until the first next(). Values come on demand, so a generator can stream a file larger than memory.

What does this print: summing the same generator twice?

Experiencedgenerators
Python
gen = (x * x for x in range(3))
print(sum(gen), sum(gen))

Predict the output

A generator can be consumed once. The first sum exhausts it; the second gets nothing and returns 0, with no error. Build a list if you need the values twice. map, filter, zip and file objects are one-shot iterators too.

What is a context manager and how does the with statement work?

Experiencedcontext managers

A context manager guarantees cleanup when a with block ends, even on an exception. It is any object with __enter__ and __exit__, like the file open() returns.

Python
from contextlib import contextmanager

class Resource:
    def __enter__(self):
        print("open")
        return self

    def __exit__(self, exc_type, exc, tb):
        print("close, error was", exc_type.__name__ if exc_type else None)
        return False

with Resource():
    print("working")

@contextmanager
def job_block(name):
    print("enter", name)
    try:
        yield
    finally:
        print("exit", name)

try:
    with job_block("job"):
        raise ValueError("boom")
except ValueError:
    print("error still raised")

contextlib.contextmanager turns a generator into one: code before yield is setup, the finally block is cleanup. See context managers.

What is pickling and unpickling in Python?

Experiencedserialization

Pickling turns a Python object into bytes with the pickle module, and unpickling rebuilds it.

Python
import pickle

order = {"id": 7, "items": ["pen", "book"], "paid": True}
data = pickle.dumps(order)
print(type(data).__name__)
restored = pickle.loads(data)
print(restored == order, restored is order)

Never unpickle data you do not trust, because unpickling can run arbitrary code. To exchange data with other languages, use JSON.

What is a closure in Python, and what does nonlocal do?

Experiencedclosuresscope

A closure is an inner function that remembers its enclosing function's variables after that function returns; nonlocal lets it rebind them.

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

c1 = make_counter()
c2 = make_counter()
print(c1(), c1(), c1(), c2())

Each make_counter call creates a new count, so c1 and c2 keep separate state. Without nonlocal, count += 1 raises UnboundLocalError.

What does this print: removing items from a list while looping over it?

Experiencedlists
Python
nums = [1, 2, 3, 4]
for n in nums:
    if n < 3:
        nums.remove(n)
print(nums)

Predict the output

Removing an item shifts the rest left while the loop's index moves on, so the next item is skipped: 2 slides into the removed 1's slot and is never checked. Build a new list with a comprehension, or loop over a copy, nums[:].

How do you create custom exceptions, and what does raise ... from do?

Seniorexceptions

Subclass Exception, usually with one base class per library so callers can catch all your errors at once. raise NewError(...) from e chains the original as __cause__.

Python
class PaymentError(Exception):
    pass

class CardDeclined(PaymentError):
    def __init__(self, code):
        super().__init__(f"card declined with code {code}")
        self.code = code

def charge(card):
    try:
        return card["number"]
    except KeyError as e:
        raise CardDeclined(51) from e

try:
    charge({})
except PaymentError as e:
    print(type(e).__name__, "|", e, "|", repr(e.__cause__))

Use raise ... from None to hide an irrelevant original, and store extra data, like code, as attributes.

Are type hints enforced at runtime in Python?

Seniortype hints

No. The interpreter stores annotations in __annotations__ and ignores them; checkers such as mypy read them before the code runs.

Python
def double(x: int) -> int:
    return x * 2

print(double("ab"))
print(double.__annotations__)

Pydantic reads them on purpose to validate data, which is how FastAPI checks request bodies. See type hints.

What is a virtual environment and why do you need one?

Experiencedtooling

A virtual environment is an isolated directory with its own site-packages, so each project pins its own dependency versions without touching the system Python or other projects.

python -m venv .venv
source .venv/bin/activate        # on Windows: .venv\Scripts\activate
pip install requests
pip freeze > requirements.txt

Commit requirements.txt or a lock file from Poetry or uv, never the .venv folder.

How does memory management and garbage collection work in Python?

Seniormemoryinternals

CPython frees most objects by reference counting, the moment their count reaches zero. A cyclic garbage collector (the gc module) frees groups of objects that only reference each other.

Python
import gc

class Node:
    def __init__(self, name):
        self.name = name
        self.other = None

    def __del__(self):
        print("freed", self.name)

a = Node("a")
del a
print("after del a")

b, c = Node("b"), Node("c")
b.other, c.other = c, b
del b, c
print("after del b, c")
gc.collect()
print("after collect")

a is freed at del a; b and c reference each other, so they survive until gc.collect().

What is __slots__ and when should you use it?

Seniormemoryoop

__slots__ fixes the set of attributes, so instances skip the per-instance __dict__ and use less memory, but cannot get new attributes.

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

class Slotted:
    __slots__ = ("x", "y")

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

p, s = Plain(1, 2), Slotted(1, 2)
print(hasattr(p, "__dict__"), hasattr(s, "__dict__"))
try:
    s.z = 3
except AttributeError as e:
    print("AttributeError:", e)

Use it for very many small objects. Every class in the inheritance chain needs __slots__, or a __dict__ comes back; @dataclass(slots=True) (3.10+) writes it for you.

Advanced Python interview questions

Concurrency, the GIL, asyncio, metaclasses, descriptors and the CPython details senior interviews probe.

What is the GIL in Python?

Seniorconcurrencyinternals

The Global Interpreter Lock is a mutex in CPython that lets one thread at a time execute Python bytecode, so threads cannot run CPU-bound Python code in parallel. Threads still help with I/O, because a waiting thread releases the GIL; for CPU-bound work use multiprocessing. The GIL does not make code thread-safe: counter += 1 can still lose updates without a lock. Python 3.13 added an experimental free-threaded build (PEP 703), officially supported since 3.14 (PEP 779); the default build keeps the GIL.

When do you use threading, multiprocessing or asyncio in Python?

Seniorconcurrency

Use multiprocessing for CPU-bound work, asyncio for many concurrent I/O tasks with async libraries, and threads for I/O-bound work with blocking libraries. On the default build, with the GIL, only processes run Python code on several cores; the free-threaded build lets threads do it too. The OS can switch threads at any moment, so shared state needs locks, while asyncio switches only at await. The risks: race conditions for threads, pickling overhead for processes, and one blocking call stalling every asyncio task.

How do async and await work in Python?

Seniorconcurrencyasync

async def defines a coroutine function; calling it returns a coroutine and runs nothing. An event loop runs coroutines on one thread and switches tasks at each await.

Python
import asyncio

async def fetch(name, delay):
    print("start", name)
    await asyncio.sleep(delay)
    print("end", name)
    return name.upper()

async def main():
    results = await asyncio.gather(fetch("a", 0.2), fetch("b", 0.1))
    print(results)

asyncio.run(main())

Both tasks start before either ends, and gather keeps argument order. The classic mistake is a blocking call such as time.sleep in a coroutine, which stalls every task; use asyncio.to_thread for blocking work.

What is a metaclass in Python?

Senioroopinternals

A metaclass is the class of a class: it controls how classes are created. The default metaclass is type.

Python
class Registry(type):
    plugins = {}

    def __new__(mcls, name, bases, namespace):
        cls = super().__new__(mcls, name, bases, namespace)
        if bases:
            Registry.plugins[name.lower()] = cls
        return cls

class Plugin(metaclass=Registry):
    pass

class CsvExport(Plugin):
    pass

class PdfExport(Plugin):
    pass

print(type(int), type(Plugin))
print(sorted(Registry.plugins))

Here the metaclass's __new__ registers every subclass as it is defined. Most such jobs are easier with __init_subclass__ or a class decorator, so application code rarely needs a metaclass.

What is a descriptor in Python?

Senioroopinternals

A descriptor is a class attribute that defines __get__, __set__ or __delete__, so attribute access on instances runs those methods. property, classmethod and bound methods are built on it.

Python
class Positive:
    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.name)

    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError(f"{self.name[1:]} must be positive")
        setattr(obj, self.name, value)

class Order:
    quantity = Positive()
    price = Positive()

    def __init__(self, quantity, price):
        self.quantity = quantity
        self.price = price

o = Order(2, 150)
print(o.quantity * o.price)
try:
    o.price = -5
except ValueError as e:
    print("ValueError:", e)

A data descriptor (with __set__) takes priority over the instance __dict__. Use one to reuse validation across attributes; for one attribute, @property is simpler.

How do you make your own class iterable?

Experiencedgeneratorsoop

Implement __iter__, easiest as a generator, which returns a fresh iterator each time, so the object can be looped over repeatedly.

Python
class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

class Squares:
    def __init__(self, limit):
        self.i, self.limit = 0, limit

    def __iter__(self):
        return self

    def __next__(self):
        if self.i >= self.limit:
            raise StopIteration
        self.i += 1
        return self.i ** 2

c = Countdown(3)
print(list(c), list(c))
s = Squares(3)
print(list(s), list(s))

Squares is its own iterator, so the second list(s) is empty. An iterable can be reused; an iterator is consumed once.

What does this print: is on integers built with int()?

Seniorinternals
Python
a = int("256")
b = int("256")
c = int("257")
d = int("257")
print(a is b, c is d)

Predict the output

CPython caches one object for each integer from -5 to 256, so both 256s are the same object, while each 257 built at runtime is new. The cache is a CPython implementation detail (constants in one code block may be shared too, hence int()), so compare numbers with ==.

What does this print: return in both try and finally?

Seniorexceptions
Python
def f():
    try:
        return "try"
    finally:
        return "finally"

print(f())

Predict the output

finally always runs, and a return there replaces the value from try and silently discards any exception on its way out. Linters flag it and Python 3.14 emits a SyntaxWarning for it (PEP 765). Use finally for cleanup only.

What does this print: 1, True and 1.0 as dict keys?

Seniordictinternals
Python
d = {}
for key, value in [(1, "a"), (True, "b"), (1.0, "c")]:
    d[key] = value
print(d)

Predict the output

1, True and 1.0 are equal and have the same hash, so a dict treats them as one key. The first insertion fixes the key object (1), and later equal keys only replace the value, so the last value wins. {1, True, 1.0} has one element for the same reason.

How does functools.lru_cache work, and when should you not use it?

Seniorfunctionsperformance

lru_cache memoizes a function by its arguments and evicts the least recently used entry once maxsize is reached; functools.cache (3.9+) is unbounded.

Python
from functools import lru_cache

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

print(ways(40))
print(ways.cache_info())

Avoid it for unhashable arguments, functions with side effects, and methods, because each entry keeps self alive.

How does Python execute your code? What is bytecode?

Seniorinternals

CPython compiles source to bytecode for a stack-based virtual machine, then an interpreter loop runs it. Imported modules cache it as .pyc files in __pycache__.

Python
import dis

def add(a, b):
    return a + b

dis.dis(add)
print(add.__code__.co_varnames)

Since 3.11 the interpreter specializes frequently run instructions for the types it sees (PEP 659). Bytecode changes between versions.

Python interview questions for data analyst roles

CSV files, grouping, missing values and common pandas questions for data analyst roles.

What is the difference between a NumPy array and a Python list?

Fresherdata analysis

A NumPy array holds one type in contiguous memory and applies operations to every element in compiled code; a list holds references to any objects and you loop over it yourself.

Python
prices = [100, 250, 80]
print(prices * 2)
print([p * 2 for p in prices])

On a list * 2 repeats it, while np.array(prices) * 2 doubles each value.

How do you read a CSV file in Python without pandas?

Fresherdata analysisfiles

Use csv.DictReader, which yields one dict per row keyed by the header (io.StringIO stands in for a file here).

Python
import csv
import io

data = io.StringIO("name,city,amount\nAsha,Pune,1200\nRavi,Delhi,800\nMeera,Pune,450\n")
rows = list(csv.DictReader(data))
print(rows[0])
total = sum(int(r["amount"]) for r in rows if r["city"] == "Pune")
print(total)

Every value is a string, so convert numbers yourself. Unlike line.split(","), the module handles quoted commas. See CSV in Python.

How do you group and aggregate data in plain Python?

Experienceddata analysiscollections

Use a defaultdict keyed by the group: one pass, no sorting, the plain Python version of SQL's GROUP BY with SUM.

Python
from collections import defaultdict
from itertools import groupby

sales = [("Pune", 1200), ("Delhi", 800), ("Pune", 450), ("Mumbai", 990), ("Delhi", 300)]

totals = defaultdict(int)
for city, amount in sales:
    totals[city] += amount
print(dict(totals))

sales.sort(key=lambda s: s[0])
for city, group in groupby(sales, key=lambda s: s[0]):
    print(city, sum(a for _, a in group))

itertools.groupby only groups consecutive items, so sort by the same key first. In pandas it is df.groupby("city")["amount"].sum().

What is the difference between loc and iloc in pandas?

Fresherpandas

loc selects by label and includes a slice's end; iloc selects by integer position and excludes it.

import pandas as pd

df = pd.DataFrame({"city": ["Pune", "Delhi", "Goa"], "sales": [120, 80, 45]},
                  index=["a", "b", "c"])

df.loc["a":"b", "sales"]     # rows a and b: the end label is included
df.iloc[0:2, 1]              # rows 0 and 1: the end position is excluded
df.loc[df["sales"] > 50]     # boolean filter

To assign, use one call, df.loc[mask, "col"] = value; chained assignment warns in pandas 2 and never updates df under pandas 3's copy-on-write.

How do you handle missing values in a dataset?

Experiencedpandasdata analysis

Measure them first, then decide per column: drop the rows, fill with a constant, the median or the previous value, or keep "missing" as a category, depending on why values are missing.

df.isna().sum()                          # missing count per column
df.dropna(subset=["amount"])             # drop rows with no amount
df["amount"] = df["amount"].fillna(df["amount"].median())
df["city"] = df["city"].fillna("Unknown")

NaN is not equal to itself, so test with isna(), never ==.

What is the difference between merge, join and concat in pandas?

Experiencedpandas

concat stacks frames, merge is a SQL-style join on columns, and join is a left join on the index. The common bug is duplicate keys on both sides, which multiplies rows; pass validate="many_to_one" so pandas raises an error instead.

How do you calculate the mean, median and mode in Python?

Fresherdata analysisstatistics

Use the standard statistics module: mean, median, mode and stdev work on any list of numbers.

Python
import statistics as st

scores = [70, 85, 85, 90, 100, 40]
print(st.mean(scores), st.median(scores), st.mode(scores))
print(round(st.stdev(scores), 2))

stdev is the sample standard deviation and pstdev the population one. NumPy's np.std defaults to population and pandas' .std() to sample, so the two can disagree.

Preparing for the interview

How should I prepare for a Python interview?
Revise the language first (mutability, functions, OOP, exceptions, generators, decorators), then data structures with their time complexity, then timed coding practice, solving out loud. Run any snippet you are unsure about. For experienced roles, prepare to explain the design of your past projects.
What is asked in Python interviews for freshers compared with experienced developers?
Freshers get definitions and small programs, such as list vs tuple or reversing a string. With 2 to 5 years expect output traps, OOP design, decorators and generators; with more, concurrency, memory, performance and the design of systems you built.
How long does it take to prepare for a Python interview?
If you already write Python, two to four weeks of daily practice: revise this page and solve a few dozen problems across hashing, two pointers, sliding window, stacks and trees. If you are still learning the language, allow a couple of months.
Do I need DSA for a Python developer interview?
For campus placements and most product companies, yes: expect arrays, strings, hash maps and basic trees. Data and automation roles lean on libraries and SQL but usually include one coding question. Counter, deque, heapq and bisect keep DSA code short in Python.
Which Python topics should I revise first?
Mutability and copying, list, dict and set complexity, comprehensions, *args and **kwargs, OOP (MRO, class vs instance attributes, dunder methods), exceptions, generators and decorators. For senior roles add the GIL, threading vs multiprocessing vs asyncio, and memory management.
Coddy programming languages illustration

Learn to code with Coddy

GET STARTED