50 questions - Fresher to experienced

Python interview questions and answers

Python interviews start with data types and mutability, move to functions, decorators and generators, and end with a short coding problem. Experienced rounds add the GIL, concurrency and performance.

Last updated:

Python basics

How does Python run your code?

CPython, the standard interpreter, first compiles your source to bytecode and then runs that bytecode in its virtual machine. Python is dynamically typed, so a variable is a name bound to an object and the type is checked at runtime. Memory is managed for you.

What is the difference between mutable and immutable types?

A mutable object can change after it is created: list, dict and set. An immutable object cannot: int, float, str, tuple and frozenset. "Changing" an immutable value really creates a new object. Only immutable (hashable) values can be dict keys or set members.

What is the difference between a list and a tuple?

A list is mutable and is used for a collection that grows or changes. A tuple is immutable, slightly lighter, can be a dict key if its items are hashable, and is used for a fixed group of values such as coordinates or a function returning several values.

Why is looking up a key in a dict fast?

A dict is a hash table. Python hashes the key to find its slot directly, so lookup, insert and delete are O(1) on average, whatever the size. Since Python 3.7, dicts also keep insertion order.

When should you use a set?

Use a set when you need unique values or fast membership tests. x in a_set is O(1) on average, while x in a_list checks every item. Sets also support union, intersection and difference.

seen = set(["a", "b"])
print("a" in seen)         # True, O(1)
print({1, 2, 3} & {2, 3, 4}) # {2, 3}

What is the difference between is and ==?

== checks whether two objects have equal values. is checks whether they are the same object in memory. Use is only for None (x is None); comparing numbers or strings with is depends on caching and gives surprising results.

a = [1, 2]
b = [1, 2]
print(a == b)  # True
print(a is b)  # False

What are *args and **kwargs?

*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dict. The same stars unpack a list or dict when calling a function.

def log(*args, **kwargs):
    print(args, kwargs)

log(1, 2, level="info")  # (1, 2) {'level': 'info'}

Why is a mutable default argument a bug?

Default values are created once, when the function is defined, not on every call. A list default is therefore shared by every call and keeps growing. Use None and create the list inside the function.

def add(item, items=[]):    # bug: one shared list
    items.append(item)
    return items

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

What is a list comprehension?

A list comprehension builds a list in one readable expression, with an optional filter. It is usually faster than an equivalent for loop with append. Dict and set comprehensions work the same way.

squares = [n * n for n in range(10) if n % 2 == 0]
lengths = {word: len(word) for word in ["hi", "hello"]}

What is the difference between a shallow copy and a deep copy?

A shallow copy creates a new outer object but shares the inner objects, so changing a nested list affects both. A deep copy recursively copies everything. Use copy.deepcopy for nested structures.

import copy
a = [[1, 2], [3]]
b = a.copy()           # shallow
c = copy.deepcopy(a)   # deep
a[0].append(9)
print(b[0])  # [1, 2, 9]
print(c[0])  # [1, 2]

How does Python pass arguments to a function?

Python passes references to objects, sometimes called pass-by-object-reference or pass-by-assignment. A function can change a mutable object it receives, but assigning a new object to the parameter does not affect the caller.

How does slicing work?

seq[start:stop:step] returns a new sequence from start up to, but not including, stop. Negative indexes count from the end. seq[::-1] is the quick way to reverse a list or string.

s = "python"
print(s[1:4])   # "yth"
print(s[-2:])   # "on"
print(s[::-1])  # "nohtyp"

Functions, scope and decorators

What is a lambda?

A lambda is a small anonymous function written as one expression. It is handy as a sort key or a short callback. For anything longer, a named def is clearer.

users.sort(key=lambda u: u["age"])

What is a closure?

A closure is an inner function that remembers variables from the function that created it, even after that outer function has returned.

def multiplier(n):
    def times(x):
        return x * n   # remembers n
    return times

double = multiplier(2)
print(double(5))  # 10

What is a decorator?

A decorator is a function that takes a function and returns a new function with extra behaviour, such as logging, timing or access checks. @decorator above a def is shorthand for func = decorator(func). functools.wraps keeps the original name and docstring.

import functools, time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(func.__name__, time.perf_counter() - start)
        return result
    return wrapper

@timed
def work():
    sum(range(10**6))

What is the LEGB rule?

It is the order in which Python looks up a name: Local (the current function), Enclosing (outer functions), Global (the module), then Built-in. The first match wins.

What is the difference between global and nonlocal?

Assigning to a name inside a function makes it local. global lets the function assign to a module-level variable. nonlocal lets an inner function assign to a variable of the enclosing function, which a closure counter needs.

What do map, filter and reduce do?

map applies a function to every item, filter keeps the items where a function returns true, and functools.reduce combines items into one value. They return iterators (except reduce). Comprehensions are often more readable for map and filter.

Object-oriented Python

What are __init__ and self?

__init__ initialises a new object after it is created. self is the object the method is called on; Python passes it automatically as the first argument, so obj.method(x) runs Class.method(obj, x).

What is the difference between class attributes and instance attributes?

A class attribute is shared by all instances. An instance attribute, set on self, belongs to one object. A mutable class attribute such as a list is a common bug, because every instance ends up changing the same list.

class Team:
    members = []          # shared by all teams (bug)
    def __init__(self):
        self.players = [] # one list per team

What is the difference between @staticmethod and @classmethod?

A classmethod receives the class as its first argument (cls), which makes it the usual way to write alternative constructors. A staticmethod receives nothing automatically; it is a plain function kept inside the class for organisation.

class Date:
    def __init__(self, y, m, d): ...

    @classmethod
    def from_string(cls, text):
        return cls(*map(int, text.split("-")))

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

How does inheritance work, and what is the MRO?

A child class reuses and can override the parent's methods. With multiple inheritance, Python searches classes in the method resolution order (MRO), calculated with C3 linearisation; you can see it with ClassName.__mro__. super() follows the MRO, not just the direct parent.

What are dunder methods?

Methods with double underscores, such as __len__, __eq__ and __add__, let your objects work with built-in syntax. __repr__ should give an unambiguous developer view; __str__ gives the readable text print uses.

class Money:
    def __init__(self, amount):
        self.amount = amount
    def __add__(self, other):
        return Money(self.amount + other.amount)
    def __repr__(self):
        return f"Money({self.amount})"

What does @property do?

@property lets a method be read like an attribute, so you can add validation or calculation later without changing the code that uses the class. A matching setter controls assignment.

class Account:
    def __init__(self):
        self._balance = 0

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("negative balance")
        self._balance = value

Does Python have private members?

Not enforced ones. A single underscore, _name, is a convention meaning "internal, do not use". A double underscore, __name, triggers name mangling to _ClassName__name, which avoids clashes in subclasses but is not real security.

Iterators, generators and errors

What is the difference between an iterable and an iterator?

An iterable is anything you can loop over, such as a list; its __iter__ returns an iterator. An iterator produces values one at a time with __next__ and raises StopIteration when done. An iterator can be used only once.

What is a generator?

A generator is a function that uses yield to produce values one at a time, pausing between them. It keeps only its current state in memory, so it can process huge files or endless sequences.

def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line.rstrip()

for line in read_lines("big.log"):
    ...

What is the difference between a generator expression and a list comprehension?

A list comprehension, in square brackets, builds the whole list in memory at once. A generator expression, in round brackets, produces items lazily. sum(x * x for x in range(10**7)) uses almost no memory; the list version stores ten million numbers.

What is a context manager?

A context manager sets something up and guarantees clean-up, used with the with statement. Files, locks and database connections use it so they are closed even if an exception happens. You can write one with __enter__ and __exit__, or with contextlib.contextmanager.

with open("data.txt") as f:
    text = f.read()
# the file is closed here, even after an error

How does exception handling work in Python?

Code that may fail goes in try. except catches specific exception types. else runs only if no exception happened. finally always runs, for clean-up. Catch the narrowest exception you can; a bare except also hides bugs and Ctrl+C.

try:
    value = int(text)
except ValueError:
    value = 0
else:
    print("parsed")
finally:
    print("done")

How do you raise a custom exception?

Create a class that inherits from Exception, or from a more specific built-in exception, and raise it with a clear message. Callers can then catch your error type alone.

class PaymentFailed(Exception):
    pass

raise PaymentFailed("card declined")

Memory, performance and concurrency

How does Python manage memory?

CPython frees most objects through reference counting: when nothing refers to an object any more, it is freed at once. A separate cyclic garbage collector finds groups of objects that refer only to each other. The gc module lets you inspect or trigger it.

What is the GIL?

The Global Interpreter Lock lets only one thread run Python bytecode at a time in standard CPython. Threads still help for I/O-bound work, because the lock is released while waiting on the network or disk. For CPU-bound work use multiprocessing or native libraries. Python 3.13 added an optional experimental build without the GIL.

When do you use threading, multiprocessing or asyncio?

Use threading for a moderate amount of blocking I/O, such as calling a few APIs. Use multiprocessing for CPU-heavy work, because each process has its own interpreter and GIL. Use asyncio for many concurrent I/O tasks, such as thousands of network connections, in one thread.

What do async and await do?

async def creates a coroutine. await pauses it until the awaited operation finishes, letting the event loop run other coroutines meanwhile. This only helps if the libraries you call are async too.

import asyncio

async def fetch(n):
    await asyncio.sleep(1)   # stands in for a network call
    return n

async def main():
    results = await asyncio.gather(fetch(1), fetch(2), fetch(3))
    print(results)  # about 1 second, not 3

asyncio.run(main())

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

For a list: append and indexing are O(1), insert(0, x) and x in list are O(n). For a dict or set: lookup, insert and delete are O(1) on average. Sorting is O(n log n). Choosing the right structure is usually the biggest speed-up.

How do you speed up slow Python code?

Measure first with cProfile or timeit. Then use a better algorithm or data structure, prefer built-ins such as sum, sorted and set operations, avoid repeated work inside loops, and use NumPy or pandas vectorised operations for numeric data.

Common Python coding questions

How do you check whether a string is a palindrome?

Normalise the text, then compare it with its reverse. Interviewers often follow up by asking you to ignore spaces and punctuation, or to do it with two pointers.

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

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

How do you count how often each character appears?

collections.Counter counts every item in one pass and can return the most common ones directly. Without it, a plain dict with counts[ch] = counts.get(ch, 0) + 1 does the same job.

from collections import Counter

counts = Counter("banana")
print(counts)               # Counter({'a': 3, 'n': 2, 'b': 1})
print(counts.most_common(1)) # [('a', 3)]

How do you solve two-sum in O(n)?

Walk the list once and remember each number's index in a dict. For each number, check whether its complement (target minus the number) was already seen.

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 []

print(two_sum([2, 7, 11, 15], 9))  # [0, 1]

How do you remove duplicates from a list but keep the order?

dict.fromkeys keeps the first occurrence of each item in order, because dicts preserve insertion order. set(items) also removes duplicates but loses the order.

items = [3, 1, 3, 2, 1]
print(list(dict.fromkeys(items)))  # [3, 1, 2]

How do you flatten a nested list?

Use recursion for any depth: if an item is a list, flatten it, otherwise yield it. For exactly one level of nesting, a comprehension is enough.

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])))  # [1, 2, 3, 4, 5]

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

Keep the largest and second largest while scanning once, skipping repeats of the largest. This is O(n) and avoids sorting.

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([4, 9, 9, 7]))  # 7

How do you merge two dictionaries?

Since Python 3.9 use the | operator. {**a, **b} works in older versions. In both, values from the right-hand dict win when keys repeat.

a = {"x": 1, "y": 2}
b = {"y": 3}
print(a | b)        # {'x': 1, 'y': 3}
print({**a, **b})   # same result

How do you generate Fibonacci numbers without storing them all?

Use a generator that keeps only the last two numbers. It can produce as many values as you ask for in constant memory.

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

from itertools import islice
print(list(islice(fibonacci(), 8)))  # [0, 1, 1, 2, 3, 5, 8, 13]

Tooling and good practice

What is a virtual environment and why use one?

A virtual environment is an isolated folder with its own Python packages, so each project can pin its own versions without breaking others. Create one with python -m venv .venv, install with pip, and record versions in requirements.txt or pyproject.toml.

What is PEP 8?

PEP 8 is the official style guide for Python code: four-space indentation, snake_case for functions and variables, CapWords for classes, and readable line lengths. Tools such as black and ruff apply it automatically.

What are type hints?

Type hints describe the expected types of arguments and return values. Python does not enforce them at runtime, but tools such as mypy and editors use them to catch bugs early.

def average(values: list[float]) -> float:
    return sum(values) / len(values)

What is the difference between a module and a package?

A module is a single .py file. A package is a folder of modules that can be imported together, traditionally marked by an __init__.py file.

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

Code inside that block runs only when the file is executed directly, not when it is imported as a module. It keeps scripts reusable: other files can import the functions without running the script part.

def main():
    print("running")

if __name__ == "__main__":
    main()

Continue with another topic

Practise live

CrackInterviewAI listens to the live interview and gives a structured answer on screen for coding, system design, HR and project questions. Download for Windows.