Top 65 Python Interview Questions and Answers (2026)

The 65 Python questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.

65 questions with answers

What Is Python?

Key Takeaways

  • Python is a high-level, interpreted, dynamically typed language known for readable syntax and a large standard library.
  • It runs everywhere and covers web development, data science, machine learning, automation, and scripting with one language.
  • Interviews test how well you understand its object model (mutability, references) and runtime (the GIL, memory), not just syntax.
  • This page is a question bank: work your tier first, read one tier up, and say answers out loud because delivery is evaluated too.

Python is a high-level, interpreted programming language created by Guido van Rossum and first released in 1991, prized for code that reads close to plain English. It's dynamically typed, supports procedural, object-oriented, and functional styles, and ships with a large standard library, which is why beginners get productive in days while it also powers production systems at scale. In interviews, Python questions probe understanding of the object model (mutability, identity, references) and the runtime (the GIL, memory management), not memorized trivia. This page collects the 65 questions that come up most, each with a direct answer and runnable code. If you're still building fundamentals, the official Python tutorial from the Python Software Foundation is the canonical path; increasingly the first Python round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.

65Questions with answers on this page
3Difficulty tiers: freshers, intermediate, experienced
22Runnable code snippets you can practice from
45-60 minTypical length of a Python technical round

Watch: Python Full Course for Beginners

Video: Python Full Course for Beginners (freeCodeCamp.org, YouTube)

Test yourself and earn a certificate

10 quick questions. Score 70%+ to download your Python certificate.

Jump to quiz

All Questions on This Page

65 questions
Python Interview Questions for Freshers
  1. 1. What is Python and what makes it popular?
  2. 2. What is the difference between a list and a tuple?
  3. 3. Which Python types are mutable and which are immutable?
  4. 4. What is the mutable default argument problem?
  5. 5. What is a dictionary and how does it work internally?
  6. 6. What is the difference between a shallow copy and a deep copy?
  7. 7. What is the difference between == and is?
  8. 8. What does it mean that functions are first-class objects in Python?
  9. 9. What are *args and **kwargs?
  10. 10. What is a list comprehension and when should you use one?
  11. 11. What does range() return, and why isn't it a list?
  12. 12. Why are strings immutable in Python, and what does that imply?
  13. 13. What is PEP 8 and why does it matter?
  14. 14. Is Python statically or dynamically typed, and what are type hints?
  15. 15. What does if __name__ == "__main__" do?
  16. 16. How does exception handling work in Python?
  17. 17. What does the with statement do?
  18. 18. What is a lambda function and when is it appropriate?
  19. 19. What is the difference between an iterable and an iterator?
  20. 20. What do break, continue, and pass do?
  21. 21. How do the in and not in operators work?
  22. 22. How does slicing work in Python?
  23. 23. What is None and how should you check for it?
  24. 24. How does variable scope work in Python (the LEGB rule)?
  25. 25. What are f-strings and why are they preferred?
Python Intermediate Interview Questions
  1. 26. What is a decorator and how do you write one?
  2. 27. What are generators and how does yield work?
  3. 28. What is the Global Interpreter Lock (GIL)?
  4. 29. When do you choose threading, multiprocessing, or asyncio?
  5. 30. How does Python manage memory?
  6. 31. What is a closure?
  7. 32. How does Python implement object-oriented programming?
  8. 33. What is the difference between @classmethod, @staticmethod, and instance methods?
  9. 34. What are dunder (magic) methods and which ones matter most?
  10. 35. How does method resolution order (MRO) work with multiple inheritance?
  11. 36. What are abstract base classes and when would you use them?
  12. 37. What does the @property decorator do?
  13. 38. How do you define and use custom exceptions?
  14. 39. What is the difference between a module and a package?
  15. 40. What are virtual environments and why are they essential?
  16. 41. What do enumerate() and zip() do?
  17. 42. What is the difference between sorted() and list.sort(), and how do key functions work?
  18. 43. Which collections-module types should every Python developer know?
  19. 44. Why does modifying a list inside a function change it outside the function?
  20. 45. How do you serialize and deserialize JSON in Python?
  21. 46. How do you write and run unit tests in Python?
  22. 47. What is the walrus operator (:=)?
Python Interview Questions for Experienced Developers
  1. 48. What actually happens when you run a Python program?
  2. 49. What are descriptors and how do properties relate to them?
  3. 50. What is a metaclass, and when is one justified?
  4. 51. How does asyncio work under the hood?
  5. 52. How do you find and fix a performance problem in Python?
  6. 53. Can Python leak memory? How would you diagnose it?
  7. 54. When do you use dataclasses, and how do they compare to namedtuple, attrs, and Pydantic?
  8. 55. How do you introduce type checking to a large untyped Python codebase?
  9. 56. How does modern Python packaging work?
  10. 57. How do you write a custom context manager?
  11. 58. How does functools.lru_cache work and where does it bite?
  12. 59. How do you make your own class iterable?
  13. 60. What is the contract between __eq__ and __hash__?
  14. 61. How do you structure configuration and secrets in a production Python service?
  15. 62. A Python service is slow or misbehaving in production. Walk through your debugging approach.
  16. 63. What are weak references and when are they useful?
  17. 64. How does Python interoperate with C, and when would you drop down?
  18. 65. Design question: how would you implement a rate limiter in Python?

Python Interview Questions for Freshers

Freshers25 questions

The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.

Q1. What is Python and what makes it popular?

Python is a high-level, interpreted programming language known for readable syntax and a huge standard library. It's dynamically typed, supports procedural, object-oriented, and functional styles, and runs on every major platform.

Its popularity comes from low friction: code reads close to English, the ecosystem covers everything from web development (Django, Flask) to data science (pandas, NumPy) and machine learning (PyTorch, scikit-learn), and beginners become productive in days rather than months.

Key point: A one-line definition plus two concrete ecosystem examples beats a memorized feature list. Interviewers open with this to hear how you organize an answer.

Q2. What is the difference between a list and a tuple?

A list is mutable: you can add, remove, and change elements after creation. A tuple is immutable: once created, its contents can't change. Lists use square brackets, tuples use parentheses.

Because tuples are immutable they're hashable (usable as dictionary keys), slightly faster, and signal intent: this collection is fixed. Lists are for sequences that grow and change.

python
nums_list = [1, 2, 3]
nums_list.append(4)        # fine

nums_tuple = (1, 2, 3)
nums_tuple[0] = 9          # TypeError: does not support item assignment

locations = {(35.6, 139.6): "Tokyo"}   # tuple as dict key works
List: mutablenums = [1, 2, 3]123.append(4)1234changes in placeTuple: immutablenums = (1, 2, 3)123nums[0] = 9TypeErrorTuples are hashable, so they can be dict keys. Lists cannot.
A list can be changed in place; a tuple cannot.
ListTuple
MutabilityMutableImmutable
Syntax[1, 2, 3](1, 2, 3)
Dict key / set memberNo (unhashable)Yes, if elements are hashable
Typical useChanging sequencesFixed records, keys

Key point: The follow-up is almost always 'when would you choose a tuple?'. Have the dictionary-key example ready.

Q3. Which Python types are mutable and which are immutable?

Mutable: list, dict, set, bytearray, and most user-defined objects. Immutable: int, float, bool, str, tuple, frozenset, and bytes.

This distinction drives real behavior: immutable objects can be dict keys, are safe to share between functions, and explain why string concatenation in a loop creates a new object every iteration. Mutability is also the root of Python's most famous gotcha, the mutable default argument.

Key point: Interviewers use this to set up the default-argument and aliasing questions. Answer it crisply and expect a follow-up.

Q4. What is the mutable default argument problem?

Default argument values are evaluated once, at function definition, not on each call. A mutable default like a list is shared across every call that doesn't pass the argument, so mutations accumulate.

The fix is the None-sentinel pattern: default to None and create the list inside the function.

python
def broken(item, bucket=[]):
    bucket.append(item)
    return bucket

broken(1)   # [1]
broken(2)   # [1, 2]  <- surprise, same list

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

Key point: Explaining WHY (definition-time evaluation) is what separates a memorized answer from understanding.

Q5. What is a dictionary and how does it work internally?

A dict maps hashable keys to values with average O(1) lookup, insertion, and deletion. Internally it's a hash table: the key's hash picks a slot, and collisions are resolved by open addressing.

Since Python 3.7, dicts preserve insertion order as a language guarantee. Keys must be hashable, which is why lists can't be keys but tuples can.

python
user = {"name": "Asha", "role": "engineer"}
user["team"] = "platform"      # O(1) insert
print(user.get("salary", 0))   # 0, no KeyError
"name"hash()slot 3012Asha34Average O(1) lookupThe key's hash picks a slot directly. Since 3.7, dicts also keep insertion order.
A key's hash selects a slot directly, giving average O(1) access.

Q6. 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: copying a list of lists gives you a new list whose elements are the same inner lists. A deep copy recursively copies everything, producing fully independent objects.

Use copy.copy() for shallow and copy.deepcopy() for deep. The classic bug is mutating a nested structure through what you thought was an independent copy.

python
import copy
grid = [[1, 2], [3, 4]]
shallow = copy.copy(grid)
shallow[0][0] = 99
print(grid[0][0])    # 99  <- shared inner list

deep = copy.deepcopy(grid)
deep[1][0] = 77
print(grid[1][0])    # 3   <- independent
Shallow copy shares the inner objectsoriginalshallow[1,2][3,4]edit inner list to bothDeep copy duplicates everythingoriginaldeep[1,2][1,2]fully independent
A shallow copy shares nested objects; a deep copy duplicates them.

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

== compares values; is compares identity, whether two names point to the same object in memory. Two equal lists satisfy == but not is.

The practical rule: use == for comparisons, and reserve is for None (if x is None), because None is a singleton.

python
a = [1, 2]
b = [1, 2]
a == b    # True  (same value)
a is b    # False (different objects)

x = None
x is None # the idiomatic None check
a = [1, 2] b = [1, 2]two separate objectsab[1,2][1,2]a == bTruea is bFalsex = y = asame object, two namesxy[1,2]x is yTrueuse `is` only for None
== compares value, is compares identity (same object).

Key point: small-integer caching (Python interns small ints, so 5 is 5 happens to be True) matters.

Q8. What does it mean that functions are first-class objects in Python?

Functions can be assigned to variables, passed as arguments, returned from other functions, and stored in data structures, exactly like any other object.

This is the foundation for decorators, callbacks, and functional tools like map and sorted's key argument. If you understand first-class functions, decorators stop being magic.

python
def shout(text):
    return text.upper()

def whisper(text):
    return text.lower()

def greet(styler):
    return styler("Hello there")

greet(shout)     # "HELLO THERE"
greet(whisper)   # "hello there"

Watch a deeper explanation

Video: First-Class Functions in Python (Corey Schafer, YouTube)

Q9. What are *args and **kwargs?

*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. They let a function accept a flexible number of arguments.

The names args and kwargs are convention; the stars are the syntax. The same stars also unpack in the other direction when calling a function.

python
def report(*args, **kwargs):
    print(args)     # (1, 2)
    print(kwargs)   # {"unit": "ms"}

report(1, 2, unit="ms")

point = (3, 5)
print(*point)       # unpacking: print(3, 5)

Q10. What is a list comprehension and when should you use one?

A list comprehension builds a list from an iterable in one readable expression: [expr for item in iterable if condition]. It replaces a three-line append loop with one line and is usually faster.

Use them for simple transform-and-filter operations. When the logic needs multiple conditions or nested loops beyond one level, a regular loop reads better, and reviewers notice when a comprehension has been stretched past readability.

python
squares_of_evens = [n * n for n in range(10) if n % 2 == 0]
# [0, 4, 16, 36, 64]

# dict and set comprehensions use the same shape
lengths = {word: len(word) for word in ["hire", "me"]}

Watch a deeper explanation

Video: Python Comprehensions: How They Work and Why You Should Use Them (Corey Schafer, YouTube)

Q11. What does range() return, and why isn't it a list?

range() returns a lazy sequence object that generates numbers on demand instead of storing them. range(1_000_000_000) uses a few dozen bytes, not gigabytes.

It supports indexing, slicing, len(), and membership tests without materializing values. Wrap it in list() only when you genuinely need the whole list.

Q12. Why are strings immutable in Python, and what does that imply?

String immutability makes strings hashable (usable as dict keys), safe to share without copying, and allows interning optimizations. Any operation that seems to modify a string actually creates a new one.

The practical implication: building a large string with += in a loop is O(n squared) because every step copies. Collect parts in a list and join once.

python
parts = []
for row in rows:
    parts.append(render(row))
html = "".join(parts)   # one allocation, the idiomatic way

Building a big string: += in a loop vs join

Operations to assemble a string of N pieces, N = 1000 (log scale). += copies the whole string every step.

+= in a loop
1,000,000 ops
join(parts)
1,000 ops
  • += in a loop: O(n squared): each step copies the whole string so far
  • join(parts): O(n): one pass, one allocation

Key point: The join idiom is the expected follow-up. Volunteering it answers the next question before it's asked.

Q13. What is PEP 8 and why does it matter?

PEP 8 is Python's official style guide: naming conventions, indentation (4 spaces), line length, import ordering, and spacing rules. It matters because Python code is read far more often than written, and a shared style makes every codebase navigable.

In practice teams enforce it with tools like flake8, black, or ruff rather than by memory. Saying that shows you've worked on real codebases.

Q14. Is Python statically or dynamically typed, and what are type hints?

Python is dynamically typed: variables get their type from the object they reference at runtime, and the same name can rebind to different types. It's also strongly typed: '1' + 1 raises rather than silently coercing.

Type hints (def total(prices: list[float]) -> float:) add optional annotations that tools like mypy check statically. The runtime ignores them; teams use them for correctness and editor support on larger codebases.

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

It runs a block only when the file is executed directly, not when it's imported. Python sets __name__ to "__main__" for the entry-point script and to the module's name on import.

This lets one file be both an importable module and a runnable script, which is why nearly every Python script ends with this guard.

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

if __name__ == "__main__":
    main()

Q16. How does exception handling work in Python?

try runs code that may fail; except catches named exception types; else runs when nothing was raised; finally always runs, for cleanup. Raise exceptions with raise, and prefer catching specific types over a bare except.

A bare except swallows everything, including KeyboardInterrupt and genuine bugs, which turns crashes into silent corruption. Specific exception types keep real failures visible.

python
try:
    value = int(user_input)
except ValueError:
    value = 0
else:
    print("clean parse")
finally:
    print("always runs")

Q17. What does the with statement do?

with runs a block inside a context manager that guarantees setup and cleanup: the classic case is opening a file that closes itself even if the block raises.

Anything with __enter__ and __exit__ works: locks, database transactions, temporary directories. It replaces fragile try/finally cleanup with one line.

python
with open("data.csv") as f:
    rows = f.readlines()
# f is closed here, even if readlines() raised

Q18. What is a lambda function and when is it appropriate?

A lambda is an anonymous single-expression function: lambda x: x * 2. It's appropriate as a short throwaway passed to sorted, max, min, or filter, where naming a function would add noise.

The moment logic needs a second expression, a name, or a docstring, use def. PEP 8 explicitly prefers def when you're assigning a lambda to a name.

python
people = [("Asha", 31), ("Ben", 26)]
people.sort(key=lambda p: p[1])   # sort by age

Q19. What is the difference between an iterable and an iterator?

An iterable is anything you can loop over (lists, strings, dicts): it produces an iterator via iter(). An iterator is the object that actually yields items one at a time via next() and remembers its position.

Every iterator is an iterable, but not vice versa. A list can be looped repeatedly; an iterator is exhausted after one pass, a distinction that explains many 'my loop ran empty' bugs.

python
nums = [1, 2, 3]        # iterable
it = iter(nums)          # iterator
next(it)                 # 1
next(it)                 # 2
list(it)                 # [3]  <- only what's left

Q20. What do break, continue, and pass do?

break exits the nearest loop entirely; continue skips to the loop's next iteration; pass does nothing and exists as a syntactic placeholder where a statement is required.

Bonus depth: loops can have an else clause that runs only when the loop completed without break, a genuinely obscure feature that interviewers occasionally probe.

Q21. How do the in and not in operators work?

in tests membership: 3 in [1, 2, 3] is True, "ell" in "hello" checks substrings, and key in dict checks keys, not values.

Performance matters here: membership in a list is O(n), in a set or dict it's O(1) average. Converting a lookup list to a set is one of the cheapest optimizations in Python.

Membership test cost as the collection grows

Drag to change the number of items. A list is scanned item by item; a set jumps straight to the answer.

x in list
~1,000 checks
x in set
~1 check

At 1,000 items, the list can do up to 1,000 comparisons; the set does about one. Converting a lookup list to a set is one of the cheapest speedups in Python.

Q22. How does slicing work in Python?

Slicing extracts a portion of a sequence with [start:stop:step]; start is inclusive, stop is exclusive, and all three parts are optional. Negative indices count from the end.

Slices never raise IndexError for out-of-range bounds, and [::-1] is the idiomatic reversal. Slicing a list returns a new (shallow) copy, which is also the classic quick-copy idiom.

python
text = "interview"
text[0:5]     # "inter"
text[-4:]     # "view"
text[::-1]    # "weivretni"
nums = [1, 2, 3]
clone = nums[:]   # shallow copy

Q23. What is None and how should you check for it?

None is Python's null: a singleton object representing absence of a value. Functions without an explicit return produce None.

Check with is None, not == None: identity is the intended semantic, is can't be overridden by a class's __eq__, and it's the PEP 8 rule. Also don't confuse None with falsy values like 0 or an empty list when both are possible inputs.

Q24. How does variable scope work in Python (the LEGB rule)?

The lookup proceeds Local, Enclosing, Global, Built-in: the function's own scope first, then any enclosing function, then the module, then built-ins.

Assignment creates a local name by default, which is why reading a global works but assigning to it needs the global keyword, and modifying an enclosing function's variable needs nonlocal. This question is the gateway to closures.

python
def counter():
    count = 0
    def bump():
        nonlocal count
        count += 1
        return count
    return bump

next_id = counter()
next_id()   # 1
next_id()   # 2
Built-inGlobal (module)Enclosing (outer function)Local (current function)name looked up here firstLookup orderLlocalEenclosingGglobalBbuilt-in
Names resolve Local, then Enclosing, Global, and Built-in.

Q25. What are f-strings and why are they preferred?

f-strings (formatted string literals, f"Hello {name}") embed expressions directly in strings. They're the preferred formatting style since Python 3.6: more readable than %-formatting or .format(), and faster because they're evaluated at compile time to bytecode.

They support any expression, format specifiers (f"{price:.2f}"), and since 3.8 the debugging form f"{value=}" that prints both the expression and its value.

Back to question list

Python Intermediate Interview Questions

Intermediate22 questions

For candidates with working experience: language mechanics, standard library judgment, and the questions that separate users from understanders.

Q26. What is a decorator and how do you write one?

A decorator is a function that takes a function and returns a modified or wrapped version of it. The @decorator syntax is shorthand for func = decorator(func) at definition time.

Decorators handle cross-cutting concerns (timing, caching, auth checks, logging) without touching the wrapped function's body. Use functools.wraps so the wrapper preserves the original's name and docstring.

python
import functools, time

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

@timed
def slow_sum(n):
    return sum(range(n))
@timeddef slow_sum(n): ...wrapper1. before: start timerslow_sum(n)original runs unchanged3. after: log elapsedThe decorator returns wrapper, so calling slow_sum now calls wrapper.

Key point: Being able to write one from memory, with functools.wraps, is the bar. The follow-up is usually 'what breaks without wraps?' (introspection and debugging).

Watch a deeper explanation

Video: Python Decorators: Dynamically Alter the Functionality of Your Functions (Corey Schafer, YouTube)

Q27. What are generators and how does yield work?

A generator is a function that yields values one at a time instead of returning once; each next() resumes execution right after the last yield, with all local state intact. It produces values lazily, so memory stays flat regardless of sequence length.

Use generators for large or infinite streams: reading huge files line by line, paginated APIs, pipelines. Generator expressions ((x*x for x in data)) are the comprehension-shaped version.

python
def read_large_file(path):
    with open(path) as f:
        for line in f:
            yield line.strip()

# processes one line at a time, never loads the file
for line in read_large_file("huge.log"):
    handle(line)
sourcehuge fileyieldpause + resumeconsumerfor line in ...one itemLazy: one value at a timeMemory stays flat no matter how long the sequence is.

Watch a deeper explanation

Video: Python Generators: How to Use Them and the Benefits You Receive (Corey Schafer, YouTube)

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

The GIL is a mutex in CPython that lets only one thread execute Python bytecode at a time. Threads still help for I/O-bound work (the GIL is released while waiting on network or disk), but CPU-bound threads can't run in parallel.

For CPU parallelism, use multiprocessing (separate processes, separate GILs) or push the work into C-backed libraries like NumPy that release the GIL internally. Recent CPython versions ship an experimental free-threaded build, worth as awareness, but the standard answer above is what production Python matters.

GIL (one lock)Thread 1running Python bytecodeThread 2waiting for the lockThread 3waiting for the lockThreads still help for I/O (the GIL releases during waits). CPU work needs multiprocessing.
WorkloadRight toolWhy
I/O-bound (APIs, disk)threading or asyncioGIL released during waits
CPU-bound (math, parsing)multiprocessingSeparate processes bypass the GIL
Numeric arraysNumPy and friendsC code releases the GIL internally

Key point: The trap is claiming threads are useless. Showing you know threads work fine for I/O is exactly what this question screens for.

Q29. When do you choose threading, multiprocessing, or asyncio?

Threading: I/O-bound work with a moderate number of tasks, or when you must call blocking libraries. Multiprocessing: CPU-bound work that needs real parallelism. Asyncio: I/O-bound work at high concurrency (thousands of connections) where cooperative single-threaded scheduling beats thread overhead, provided your libraries are async-aware.

The honest interview answer includes the cost side: multiprocessing pays serialization and memory overhead, and asyncio requires an async ecosystem top to bottom, since one blocking call stalls the whole event loop.

Q30. How does Python manage memory?

CPython uses reference counting as its primary mechanism: every object tracks how many references point to it, and when the count hits zero the object is freed immediately. A supplementary cyclic garbage collector handles reference cycles that counting alone can't free.

Objects live on a private heap managed by the interpreter; small objects go through the pymalloc allocator. You rarely manage memory manually, but the model explains __del__ timing, why cycles need the gc module, and why del only removes a name.

Q31. What is a closure?

A closure is a function that captures variables from its enclosing scope and keeps them alive after the outer function returns. The inner function 'closes over' those names.

Closures power decorators, factory functions, and callbacks with baked-in state. The classic gotcha: closures capture variables, not values, so loop variables captured in lambdas all see the final value unless you bind them as default arguments.

python
# gotcha: all three print 2
funcs = [lambda: i for i in range(3)]

# fix: bind the current value as a default
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]   # [0, 1, 2]

Watch a deeper explanation

Video: Closures: How to Use Them and Why They Are Useful (Corey Schafer, YouTube)

Q32. How does Python implement object-oriented programming?

Classes define attributes and methods; instances get their own __dict__ of attributes; methods receive the instance as self explicitly. Python supports inheritance (including multiple), polymorphism through duck typing, and encapsulation by convention (a leading underscore means internal, double underscore triggers name mangling).

The Pythonic emphasis is duck typing: code cares whether an object supports the needed behavior, not what class it is. That's why interfaces are informal and isinstance checks are used sparingly.

Watch a deeper explanation

Video: Python OOP Tutorial 1: Classes and Instances (Corey Schafer, YouTube)

Q33. What is the difference between @classmethod, @staticmethod, and instance methods?

Instance methods receive self and act on one object. Class methods receive cls and act on the class, most commonly as alternative constructors. Static methods receive neither; they're plain functions namespaced inside the class for organization.

python
class Invoice:
    def __init__(self, total):
        self.total = total

    @classmethod
    def from_line_items(cls, items):      # alternative constructor
        return cls(sum(items))

    @staticmethod
    def validate_currency(code):          # utility, no self/cls
        return len(code) == 3

inv = Invoice.from_line_items([120, 80])

Key point: The from_* alternative-constructor pattern is the answer interviewers hope to hear for 'when would you use a classmethod?'.

Q34. What are dunder (magic) methods and which ones matter most?

Dunder methods (double underscore: __init__, __repr__, __eq__) let your objects plug into Python's syntax: operators, len(), iteration, string display, context managers. Implementing them is how a custom class behaves like a built-in.

The everyday set: __init__ (construction), __repr__ (debugging display, aim for unambiguous), __eq__ and __hash__ (equality and dict/set membership, always together), __len__ and __iter__ (container behavior), __enter__/__exit__ (with-statement support).

python
class Money:
    def __init__(self, amount, currency):
        self.amount, self.currency = amount, currency
    def __repr__(self):
        return f"Money({self.amount!r}, {self.currency!r})"
    def __eq__(self, other):
        return (isinstance(other, Money)
                and (self.amount, self.currency) == (other.amount, other.currency))
    def __hash__(self):
        return hash((self.amount, self.currency))

Q35. How does method resolution order (MRO) work with multiple inheritance?

Python linearizes the class hierarchy with the C3 algorithm: each class's MRO is a single ordered list that respects every parent's order, child before parent, and each class appearing once. Method lookup walks that list; ClassName.__mro__ shows it.

super() follows the MRO, not simply 'the parent', which is what makes cooperative multiple inheritance work: every class in the chain calls super() and the MRO guarantees each __init__ runs exactly once.

python
class A:  pass
class B(A): pass
class C(A): pass
class D(B, C): pass

D.__mro__   # D -> B -> C -> A -> object

Q36. What are abstract base classes and when would you use them?

An abstract base class (via the abc module) defines methods subclasses must implement; instantiating a class with unimplemented abstract methods raises immediately. Use ABCs when you're designing a plugin-style interface and want loud, early failure instead of a missing-method crash at runtime.

The counterweight to mention: idiomatic Python often prefers duck typing and Protocols (structural typing from the typing module) over nominal ABCs. Knowing both options and the trade-off is the senior-sounding answer.

Q37. What does the @property decorator do?

@property turns a method into an attribute-style accessor: callers write obj.total instead of obj.total(), while your code computes, validates, or caches behind the scenes. Paired @total.setter intercepts assignment.

Its real value is evolution: a plain attribute, and if validation is needed later, add a property without breaking a single caller comes first. That's the answer to 'why not just getters and setters like Java?'.

python
class Order:
    def __init__(self):
        self._items = []

    @property
    def total(self):
        return sum(self._items)

order = Order()
order._items = [10, 15]
order.total        # 25, no parentheses

Q38. How do you define and use custom exceptions?

Subclass Exception (never BaseException), name it with an Error suffix, and raise it where the failure happens. A small exception hierarchy per package (a base error plus specific subclasses) lets callers catch at whatever granularity they need.

Also worth saying: chain causes with raise NewError(...) from original so tracebacks keep the root cause.

python
class PaymentError(Exception): pass
class CardDeclinedError(PaymentError): pass

try:
    charge(card)
except GatewayTimeout as e:
    raise PaymentError("gateway unavailable") from e

Q39. What is the difference between a module and a package?

A module is a single .py file; a package is a directory of modules, traditionally marked by __init__.py, that creates a namespace hierarchy (package.module.function).

__init__.py runs on first import and commonly curates the package's public surface by re-exporting key names. Imports resolve via sys.path, which explains most 'ModuleNotFoundError' debugging.

Q40. What are virtual environments and why are they essential?

A virtual environment is an isolated Python installation per project: its own interpreter link and site-packages, so project A's dependency versions can't collide with project B's. python -m venv .venv creates one; activation just adjusts PATH.

Production answers pin dependencies (requirements.txt or a lock file from a tool like poetry or uv) so environments are reproducible on every machine and in CI.

Q41. What do enumerate() and zip() do?

enumerate(iterable) yields (index, item) pairs, replacing manual counters. zip(a, b) pairs elements from multiple iterables, stopping at the shortest; zip(*pairs) inverts it, the idiomatic 'unzip'.

python
names = ["Asha", "Ben"]
scores = [91, 84]
for i, (name, score) in enumerate(zip(names, scores), start=1):
    print(f"{i}. {name}: {score}")

Q42. What is the difference between sorted() and list.sort(), and how do key functions work?

list.sort() sorts in place and returns None; sorted() works on any iterable and returns a new list. Both take key (a function computing each element's sort value) and reverse, and both are stable: equal elements keep their relative order.

Stability is the underrated half of this answer: sorting by secondary key then primary key produces a correct multi-level sort precisely because Timsort is stable.

python
rows = [("Asha", "eng", 31), ("Ben", "eng", 26), ("Cara", "ops", 29)]
rows.sort(key=lambda r: r[2])          # by age
rows.sort(key=lambda r: r[1])          # then by team: stable multi-level sort

Q43. Which collections-module types should every Python developer know?

Counter (frequency counting with most_common), defaultdict (auto-initializing values, ends the key-check dance), deque (O(1) appends and pops at both ends, the right queue), and namedtuple or dataclasses for lightweight records.

Reaching for these instead of reimplementing them is a strong signal of real-world fluency; Counter alone collapses a dozen-line loop to one line.

python
from collections import Counter, defaultdict

Counter("mississippi").most_common(2)   # [("i", 4), ("s", 4)]

groups = defaultdict(list)
for name, team in pairs:
    groups[team].append(name)           # no key check needed

Q44. Why does modifying a list inside a function change it outside the function?

Python passes references to objects (call it pass-by-object-reference): the parameter and the caller's variable The same list, so in-place mutation is visible to both. Rebinding the parameter (param = [...]) only changes the local name.

The distinction to articulate is mutation versus rebinding. Functions that mutate arguments says so loudly; otherwise return a new object and leave inputs alone.

python
def add_item(bucket):
    bucket.append(99)     # mutates the caller's list

def replace(bucket):
    bucket = [99]         # rebinds local name only

items = [1]
add_item(items)   # items == [1, 99]
replace(items)    # items unchanged

Q45. How do you serialize and deserialize JSON in Python?

The json module: json.dumps(obj) to a string, json.loads(s) back to Python objects, with dump/load for file handles. Dicts, lists, strings, numbers, booleans, and None map directly; anything else (datetimes, custom classes) needs a default function or explicit conversion.

Mention pickle only with its warning label: it executes arbitrary code on load, so it's for trusted internal data only, never user input.

Q46. How do you write and run unit tests in Python?

pytest is the practical standard: test files named test_*.py, plain functions with assert statements, fixtures for setup, parametrize for input matrices, and one command to run everything. The standard library's unittest works everywhere but needs more ceremony.

The technical detail is testing behavior rather than implementation, and mocking external boundaries (network, clock, database) with unittest.mock or pytest fixtures.

python
# test_pricing.py
import pytest
from pricing import apply_discount

@pytest.mark.parametrize("price,pct,expected", [
    (100, 10, 90.0),
    (100, 0, 100.0),
])
def test_apply_discount(price, pct, expected):
    assert apply_discount(price, pct) == expected

Q47. What is the walrus operator (:=)?

The walrus operator assigns and returns a value inside an expression, letting you capture a result where a plain statement can't go: while chunk := f.read(8192): processes a stream without priming reads or repeated calls.

Use it where it removes duplication (loop conditions, comprehension filters that reuse a computed value); skip it where it just compresses two clear lines into one clever one.

Back to question list

Python Interview Questions for Experienced Developers

Experienced18 questions

advanced rounds probe internals, design judgment, and production scars. Expect every answer here to draw a follow-up.

Q48. What actually happens when you run a Python program?

CPython compiles source to bytecode (the .pyc files cached in __pycache__), then a stack-based virtual machine interprets that bytecode. So Python is both compiled and interpreted; the compilation just targets the VM rather than the CPU.

Senior-level extras: the bytecode cache invalidates on source change, import runs a module's top level exactly once per process, and alternative runtimes (PyPy's JIT) change the execution story without changing the language.

Q49. What are descriptors and how do properties relate to them?

A descriptor is any class implementing __get__, __set__, or __delete__; when an instance of it sits on a class, attribute access on instances routes through those methods. This is the machinery beneath properties, methods, classmethod, and staticmethod.

@property is a data descriptor built for you. Writing a custom descriptor earns its keep when the same attribute behavior (validation, units, lazy loading) must repeat across many attributes or classes.

python
class Positive:
    def __set_name__(self, owner, name):
        self.name = name
    def __get__(self, obj, objtype=None):
        return obj.__dict__[self.name]
    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError(f"{self.name} must be positive")
        obj.__dict__[self.name] = value

class Product:
    price = Positive()
    stock = Positive()   # same validation, zero duplication

Q50. What is a metaclass, and when is one justified?

A metaclass is the class of a class: type by default. It intercepts class creation itself, letting you validate, register, or rewrite classes as they're defined. ORMs and serialization frameworks use metaclasses to turn declarative class bodies into machinery.

The honest production-ready answer: almost never write one. __init_subclass__ and decorators cover most registration and validation cases with far less magic. Knowing the mechanism but reaching for simpler tools first is the signal the question needs.

Q51. How does asyncio work under the hood?

An event loop runs coroutines cooperatively: await yields control back to the loop, which multiplexes thousands of paused coroutines over OS-level I/O readiness (epoll/kqueue). Nothing runs in parallel; everything interleaves at await points on one thread.

The two production rules that follow: any blocking call (requests, time.sleep, heavy CPU) freezes every coroutine, so blocking work goes to run_in_executor or async-native libraries; and structured concurrency (gather, TaskGroup) beats fire-and-forget tasks that swallow exceptions.

python
import asyncio, aiohttp

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

async def main(urls):
    async with aiohttp.ClientSession() as session:
        return await asyncio.gather(*(fetch(session, u) for u in urls))

Key point: 'What happens if you call a blocking function inside a coroutine?' is the follow-up. The answer: the whole loop stalls; that's the point of this question.

Q52. How do you find and fix a performance problem in Python?

Measure first: cProfile or py-spy for CPU hot spots, tracemalloc for memory, and a realistic workload, because intuition about bottlenecks is usually wrong. Then fix in order of impact: better algorithms and data structures, batching I/O, caching (functools.lru_cache), vectorizing with NumPy or pandas, and only then parallelism.

the check is for the discipline (profile, don't guess) and the escalation ladder ending at 'rewrite the hot loop in C/Rust or use a faster runtime' as the last resort, not the first.

Q53. Can Python leak memory? How would you diagnose it?

Yes, practically: unbounded caches and module-level containers that only grow, reference cycles involving __del__, closures capturing large objects, and C-extension leaks all inflate long-running processes even with garbage collection.

Diagnosis: tracemalloc snapshots diffed over time to find the growing allocation sites, gc.get_objects()/objgraph for cycle hunting, and process-level RSS tracking to confirm. The fix is usually a bounded cache (lru_cache maxsize, TTL) or breaking the cycle with weakref.

Q54. When do you use dataclasses, and how do they compare to namedtuple, attrs, and Pydantic?

dataclasses generate __init__, __repr__, and __eq__ from annotated fields: the default for structured records in modern code. namedtuple gives immutable, tuple-compatible records with lower memory. Pydantic adds runtime validation and parsing, the right tool at trust boundaries (APIs, config). attrs is the feature-richer ancestor of dataclasses, still common in libraries.

The judgment call interviewers probe: validation at boundaries (Pydantic), plain data inside the system (dataclasses), and frozen=True when immutability communicates intent.

ToolValidationMutabilityBest at
dataclassNone (hints only)Either (frozen flag)Internal records
namedtupleNoneImmutableLightweight tuples with names
PydanticRuntime parsingConfigurableAPI/config boundaries
attrsOptional validatorsEitherLibrary authors, rich features

Q55. How do you introduce type checking to a large untyped Python codebase?

Incrementally: run mypy (or pyright) with permissive settings, type the boundaries first (public APIs, data models), enforce no-new-untyped-code in CI, and ratchet strictness module by module. Typed dicts, Protocols, and Optional discipline pay for themselves at boundaries fastest.

The failure mode to name: a big-bang typing sprint that produces thousands of errors and gets abandoned. Gradual typing is a migration, and the tooling (per-module overrides) exists precisely for that.

Q56. How does modern Python packaging work?

pyproject.toml is the single source of truth: project metadata, dependencies, and build backend. Build produces a wheel (prebuilt) and sdist; publishing goes to PyPI via twine or the build tool. Lock files (poetry, uv, pip-tools) pin the full dependency graph for reproducibility.

For applications, the deliverable is usually a container image with a locked environment rather than a published package; saying that distinction out loud indicates production experience.

Q57. How do you write a custom context manager?

Two ways: a class with __enter__ and __exit__ (where __exit__ receives exception info and can suppress by returning True), or the lighter @contextlib.contextmanager decorator around a generator that yields exactly once, with try/finally providing the cleanup.

python
from contextlib import contextmanager
import time

@contextmanager
def timed(label):
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{label}: {time.perf_counter() - start:.3f}s")

with timed("batch import"):
    run_import()

Q58. How does functools.lru_cache work and where does it bite?

lru_cache memoizes a function keyed by its arguments, evicting least-recently-used entries past maxsize. It's the one-line fix for repeated pure computation.

The bites: arguments must be hashable; caching methods keeps instances alive (leak via self) unless you use cached_property or a per-instance cache; unbounded (maxsize=None) caches grow forever; and caching anything impure trades correctness for speed.

Q59. How do you make your own class iterable?

Implement __iter__ returning an iterator, and the easiest correct iterator is a generator: write __iter__ as a generator function and Python handles __next__ and StopIteration for you. Each call to __iter__ should return a fresh iterator so the object supports multiple simultaneous loops.

python
class Playlist:
    def __init__(self, tracks):
        self._tracks = list(tracks)
    def __iter__(self):
        yield from self._tracks

for track in Playlist(["a", "b"]):
    print(track)

Q60. What is the contract between __eq__ and __hash__?

Objects that compare equal must have equal hashes, and an object's hash must never change while it's in a dict or set. Defining __eq__ without __hash__ makes instances unhashable (Python sets __hash__ to None), which is correct for mutable objects and a bug for value objects.

Practical guidance: hash the same immutable tuple of fields you compare in __eq__, or use frozen dataclasses which generate both consistently.

Q61. How do you structure configuration and secrets in a production Python service?

Environment variables carry deployment-specific values, parsed and validated once at startup into a typed settings object (Pydantic Settings is the common choice); secrets come from the platform's secret manager, never the repo; and the config object is passed explicitly rather than imported globally, which keeps tests trivial.

The anti-patterns worth naming: config.py files mutated at import time, scattered os.environ reads deep in the call stack, and .env files leaking into images.

Q62. A Python service is slow or misbehaving in production. Walk through your debugging approach.

Reproduce or observe first: logs and traces around the slow path, then py-spy dump/top on the live process for a no-restart flame view (is it CPU, a lock, or waiting on I/O?). Correlate with recent deploys and dependency changes; check the usual suspects: N+1 queries, missing timeouts, retry storms, GIL-bound CPU work on a threaded server.

Then fix at the right layer with a measurement to confirm. The technical sequence matters more than any single tool: observe, hypothesize, verify, fix, confirm.

Key point: The methodology is; tools support the evidence.

Q63. What are weak references and when are they useful?

A weak reference points to an object without increasing its reference count, so it doesn't keep the object alive. When the object is collected, the weakref returns None.

Use cases: caches and registries that shouldn't immortalize their entries (WeakValueDictionary), observer patterns where listeners may die, and breaking reference cycles between parent and child structures.

Q64. How does Python interoperate with C, and when would you drop down?

Options in ascending effort: ctypes/cffi to call existing shared libraries, Cython to compile annotated Python to C, and hand-written CPython extension modules (or Rust via PyO3, the modern favorite). NumPy-style vectorization often removes the need entirely.

Drop down when a profiled hot loop is pure computation that resists vectorization, and remember extensions can release the GIL around pure-C sections, which is how libraries achieve real threaded parallelism.

Q65. Design question: how would you implement a rate limiter in Python?

Clarify the requirements first (per-user or global, burst tolerance, distributed or single process), then pick the algorithm: a token bucket gives smooth rates with burst capacity and is a few lines with a timestamp and a counter. Single process: store buckets in a dict keyed by user with a lock. Distributed: the state moves to Redis with atomic Lua or INCR+EXPIRE, because per-process memory can't see global traffic.

The closing step is the operational edges: clock behavior, what to return on rejection (429 plus Retry-After), and metrics on rejects. Structuring the answer clarify, algorithm, storage, edges is what the question is really scoring.

python
import time

class TokenBucket:
    def __init__(self, rate, capacity):
        self.rate, self.capacity = rate, capacity
        self.tokens, self.updated = capacity, time.monotonic()

    def allow(self):
        now = time.monotonic()
        self.tokens = min(self.capacity,
                          self.tokens + (now - self.updated) * self.rate)
        self.updated = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False
Back to question list

Why Python? Python vs Java, JavaScript, and Go

Python wins when developer speed and readability matter more than raw runtime performance, which is most application, data, and scripting work. It trades some execution speed for a gentler syntax and a deeper ecosystem, so teams ship faster and hire more easily. Where it loses is CPU-bound parallelism (the GIL) and cold-start-sensitive or memory-tight environments, where Go or a compiled language fits better. Knowing these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.

LanguageTypingBest atWatch out for
PythonDynamicData, ML, scripting, fast prototypingCPU parallelism (GIL), raw speed
JavaStaticLarge enterprise systems, AndroidVerbosity, slower to prototype
JavaScriptDynamicBrowser and full-stack webType quirks, ecosystem churn
GoStaticConcurrent services, low-latency toolsSmaller data/ML ecosystem

How to Prepare for a Python Interview

Prepare in layers, and practice out loud. Most Python rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.

  • Master your tier's concepts until you can explain them without notes, then read one tier up for the stretch questions.
  • Type and run every snippet; modifying working code cements it far faster than reading.
  • Practice thinking aloud on small problems with a timer, because your process, not just the final answer is the technical point.
  • Take the quiz on this page to find weak spots, then revisit those questions before the real round.

The typical Python interview flow

1Recruiter or phone screen
background, motivation, a few concept checks
2Technical concepts
mutability, scope, the GIL, decorators, generators
3Live coding
write and explain a function under observation
4Design or debugging
trade-offs, reading unfamiliar code, follow-ups

Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.

Test Yourself: Python Quiz

Ready to test your Python knowledge?

10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.

10 questions Instant feedback Free certificate on 70%+

Frequently  Asked  Questions

Which Python topics should I prioritize before a technical round?

Prioritize the areas that change real decisions: fundamentals, practical tasks, debugging, trade-offs, and evidence. The highest-value questions are tied to a project example, a validation check, or a failure mode.

Are these questions enough to pass a Python interview?

They cover the question-answer portion well, but most Python rounds also include live coding: writing a function under observation while explaining your thinking. solving small problems out loud with a timer is the practice path. Our AI coding interview prep guide covers that format, including how automated evaluation reads your process.

Which Python version do these answers assume?

Python 3, throughout. Python 2 reached end of life in 2020 and no interviewer expects it; if legacy differences come up at all, it's usually the print statement or integer division, both covered here. When in doubt in an interview, say you're answering for Python 3.

How long does it take to prepare for a Python interview?

If you use Python at work or in study, one to two weeks of an hour a day covers this bank with practice runs. Starting colder, plan three to four weeks and write code daily; reading answers without typing them is how preparation quietly fails.

Do interviewers ask exactly these questions?

The concepts repeat far more reliably than the exact wording. Interviewers rephrase, chain follow-ups, and push toward whatever you claim confidence in. Prepare the underlying ideas, mutability, scoping, the GIL, decorators, and the phrasing takes care of itself.

Is there a way to test my Python knowledge?

Yes. The quiz on this page scores you as you go and explains every answer, so you find your weak spots fast. Pass the threshold and you can download a shareable certificate with your name and score, free and with no sign-up. It is the quickest way to check yourself before the real round.

From the team that builds coding interviews

Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages, Python included. These questions reflect what actually gets asked and evaluated inside the interviews we host for 5,000+ HR teams.

See how the AI Coding Interviewer works

Sources

Adithyan RKWritten by Adithyan RK
Surya N
Fact-checked by Surya N
Published on: 22 May 2026Last updated: 23 Jun 2026
Share: