I have been on both sides of Python technical interviews many times. The questions I am covering here are not the ones that trip people up because they are obscure. They are the ones that reveal whether you actually understand how Python works, or whether you have just been using it to get things done without examining the underlying mechanics. That distinction matters more to interviewers than most candidates realize.
๐ Table of Contents
For each question I will give you: what most candidates say, and then what a strong answer actually looks like. The hiring decisions usually hinge on the difference.
Core Language Concepts
1. What is the difference between a list and a tuple?
Most candidates say: “Lists are mutable, tuples are immutable.”
That is correct but incomplete. The more interesting answer addresses why this distinction exists and what practical consequences it has. Tuples are hashable when they contain only hashable elements, which means you can use them as dictionary keys or put them in sets. Lists cannot be used this way at all. This is not a minor edge case โ it comes up constantly when you need compound keys for caches, counters, or lookup tables.
There is also a semantic dimension. Lists conventionally represent a homogeneous sequence of the same kind of thing (all users, all transactions). Tuples conventionally represent a heterogeneous record where position has meaning โ like a function returning (status_code, response_body, elapsed_ms). When you see a tuple in code, the message is “these different things belong together as a unit.” When you see a list, it means “here are multiple items of the same type.”
Strong candidates also mention named tuples from collections, or dataclasses with frozen=True, for cases where you want immutability with the readability of attribute access.
2. Explain Python’s GIL. When does it matter?
The Global Interpreter Lock (GIL) prevents multiple threads from executing Python bytecode simultaneously. The key insight most candidates miss: the GIL is released during I/O operations. When a thread waits for a network response, reads from disk, or calls a blocking system call, it releases the GIL and other threads can run.
This means threading is perfectly useful for I/O-bound work โ web scraping, API calls, server applications handling concurrent requests. All of that is I/O-bound, and threads work well for it despite the GIL. For CPU-bound work like image processing or numerical computation, threads do not help because the GIL prevents true parallelism. The solution there is multiprocessing (separate processes, each with their own interpreter and GIL), or using C extensions like NumPy that release the GIL during their operations.
Python 3.13 introduced experimental free-threaded mode where the GIL can be disabled. It is not production-ready yet, but it represents the direction the language is heading.
3. What is a decorator and what does @functools.wraps do?
A decorator is a function that takes a function and returns a modified function. The @ syntax is shorthand for my_function = decorator(my_function). That is the simple version.
The functools.wraps detail is what separates developers who understand decorators from those who have just used them. Without @functools.wraps, your decorator replaces the original function’s name, docstring, and annotations with the wrapper function’s metadata. This breaks logging (logs show “wrapper” instead of the actual function name), documentation tools, and introspection. It is a small detail that has real consequences in production code.
Strong candidates can write a practical decorator from scratch โ something like a retry decorator with exponential backoff, or a caching decorator with TTL. These demonstrate real-world usage rather than just conceptual understanding.
4. What is a generator and when would you use one instead of a list?
Generators use yield instead of return and produce values lazily โ one at a time, on demand, without computing or storing the entire sequence in memory.
The practical case: processing a large CSV file. If you do list(csv.DictReader(f)) on a 10GB file, you load 10GB into memory. If you yield each row instead, you process one row at a time with constant memory usage. For typical server applications processing large datasets, this is the difference between running out of memory and running fine.
Generators compose well too. You can build processing pipelines where each step is a generator, and the actual computation only happens as values are consumed. This is how Python’s itertools works and why it is efficient on large data.
When to use a list instead: when you need random access by index, when you need to know the length before iterating, when the data is small enough that memory is not a concern, or when you need to iterate over the sequence multiple times (generators are exhausted after one pass).
5. What is the default argument trap?
This one catches experienced developers more often than beginners. Default argument values are evaluated once, when the function is defined โ not each time the function is called. This means mutable defaults like lists and dicts are shared across all calls:
The standard example: def add_item(item, collection=[]). The list is created exactly once. Every call that uses the default shares the same list object, so items accumulate across calls. The fix is to use None as the default and create a fresh list inside the function when needed.
This is not just a gotcha for interviews โ it comes up in real code, especially when developers are writing utility functions that accumulate results.
Object-Oriented Python
6. What is the difference between @classmethod and @staticmethod?
Both create methods you can call on the class without instantiating it. The difference: @classmethod receives the class itself as its first argument (conventionally named cls) and can access or modify class-level data. @staticmethod receives nothing implicitly โ it is a regular function that happens to live in the class’s namespace.
The practical use of @classmethod: factory methods. When you write a class method that calls cls(arguments) to create an instance, it creates an instance of whatever subclass it was called on. This is the correct pattern for alternative constructors, like a User.from_dict(data) or DatabaseConfig.from_environment() method. Using the class name directly instead of cls breaks inheritance.
The practical use of @staticmethod: utility functions that logically belong with a class but do not need to access class or instance state. A URL validator that belongs on a WebService class, or a port validation method on a connection class. It signals “this function belongs here, but it has no side effects and no dependencies on the class state.”
7. Explain Python’s Method Resolution Order (MRO)
When you call a method on an object with multiple inheritance, Python uses C3 linearization to determine which parent class’s method to call. The algorithm creates a consistent ordering where each class appears before its parents, and if a class appears in multiple inheritance chains, it appears in the earliest position that is consistent across all constraints.
The practical consequence: if you use super() consistently throughout a class hierarchy, Python correctly calls each class’s method exactly once, even in diamond inheritance patterns. This only works if every class in the hierarchy uses super(). If any class calls a parent method directly by name, the super() chain breaks and methods can be called multiple times or not at all.
You can inspect the MRO of any class with ClassName.__mro__ or ClassName.mro(). This is useful for debugging unexpected method behavior in codebases with complex inheritance.
Async Python
8. What is the difference between asyncio concurrency and multiprocessing parallelism?
Concurrency means tasks can make progress in overlapping time periods without necessarily running simultaneously. Parallelism means tasks literally run at the same moment on different CPU cores.
Python’s asyncio provides concurrency on a single thread. One task runs until it hits an await point (usually waiting on I/O), then Python switches to another task. For I/O-bound work โ HTTP requests, database queries, reading files โ this is extremely effective because most of the time is spent waiting, not computing. A single async event loop can handle thousands of concurrent I/O operations efficiently.
Multiprocessing provides true parallelism by running multiple Python interpreters simultaneously on different CPU cores. For CPU-bound work โ number crunching, image processing, data transformation โ this is the right tool. Each process has its own GIL, so they can run Python code in true parallel.
The mistake people make: using threads for CPU-bound work (GIL prevents parallelism) or using multiprocessing for I/O-bound work (process startup overhead outweighs benefits for most I/O tasks). Match the tool to the bottleneck.
What Interviewers Are Really Looking For
The meta-level insight: strong answers explain why Python works the way it does, not just that it works that way. When you explain the memory implications of generators, describe what releases the GIL and why, or explain why functools.wraps matters, you demonstrate that you have thought carefully about the language rather than accumulated syntax.
Strong candidates also acknowledge trade-offs. “It depends on the use case” is often the honest answer in software engineering. Candidates who give confident, context-free answers signal that they are pattern-matching from memory rather than reasoning from understanding. Candidates who say “it depends on whether you are I/O-bound or CPU-bound” or “it depends on whether you need the sequence once or multiple times” are more useful to work with.
The questions above are not exhaustive. Read the Python documentation for the topics you feel uncertain about โ the official docs are well-written and contain explanations you will not find in most tutorials. Python’s data model documentation in particular covers most of what comes up in senior-level interviews.
๐ You might also like
๐ Share this article




โ๏ธ Leave a Comment