Python interviews test whether you actually understand the language or just use it as a scripting tool. The questions below go one layer beneath syntax - into the GIL, the object model, and the quirks that trip up mid-level engineers. Each has the crisp answer an interviewer is listening for.
The GIL and concurrency
The Global Interpreter Lock is the single most common "explain Python internals" question at mid-to-senior level. Know exactly what it does and when it matters.
| Question | Sharp answer |
|---|---|
| What is the GIL? | A mutex in CPython that allows only one thread to execute Python bytecode at a time. It protects the reference-count-based memory manager from data races. |
| Does threading give parallelism in Python? | Not for CPU-bound work. Two threads cannot run Python bytecode simultaneously on multiple cores. For I/O-bound work (network, disk) threading is effective because threads release the GIL during blocking I/O calls. |
| How do you achieve CPU parallelism? | multiprocessing - each process has its own GIL and its own interpreter. concurrent.futures.ProcessPoolExecutor is the higher-level API. C extensions (NumPy, Cython) can also release the GIL explicitly. |
| Will the GIL be removed? | PEP 703 (CPython 3.13) added an experimental "free-threaded" build that can disable the GIL. It is opt-in and not yet the default. For interviews, assume the GIL exists unless told otherwise. |
| When is threading still useful despite the GIL? | Any I/O-bound or wait-heavy work: HTTP requests, database queries, file reads, sleep. asyncio is usually a better fit for I/O concurrency, but threading works and is simpler when you need true OS threads. |
Mutability and the default-argument trap
Python's mutability rules underlie a class of subtle bugs that interviewers love to probe.
| Question | Sharp answer |
|---|---|
| Which built-in types are immutable? | int, float, bool, str, bytes, tuple, frozenset. Lists, dicts, sets and most user-defined objects are mutable. |
| What is the default-argument trap? | Default argument values are evaluated once at function definition time, not at each call. So def f(lst=[]) shares the same list object across all calls. The fix: use None as the default and create the mutable object inside the body. |
| Why does x = [] then y = x then y.append(1) change x? | Assignment binds a name to an object; it does not copy. x and y point to the same list. To get an independent copy use y = x.copy() or y = list(x) (shallow) or copy.deepcopy(x) (deep). |
| When does tuple immutability break down? | A tuple can contain a mutable object, e.g. t = ([1, 2],). t[0].append(3) mutates the list inside - the tuple's reference stays fixed but the referenced object changes. |
Generators and lazy evaluation
Generators are a first-class Python feature and appear frequently in both language and algorithm questions.
| Question | Sharp answer |
|---|---|
| What is a generator? | A function that uses yield instead of return. Calling it returns a generator object (an iterator). Values are produced one at a time on demand - execution resumes from the yield point on each next() call. |
| Generator vs list comprehension - when to use which? | Use a generator expression when you only need to iterate once and the sequence could be large (it holds one value in memory at a time). Use a list when you need random access, multiple passes, or a known-length result. |
| What is yield from? | Delegates to a sub-generator. It passes values through in both directions (supports send() and throw()) and propagates StopIteration cleanly. Equivalent to a loop calling next() on the inner generator, but more efficient and correct. |
| How do generators relate to iterators? | Every generator is an iterator (it implements __iter__ and __next__). Not every iterator is a generator - classes can implement the protocol manually. The iterator protocol is duck-typed: any object with __next__ works in a for loop. |
Decorators
Decorators are syntactic sugar for higher-order functions. Interviewers often ask both what they do and how to write one correctly.
| Question | Sharp answer |
|---|---|
| What does @decorator do? | It is shorthand for f = decorator(f). The decorator is a callable that receives the original function and returns a replacement (usually a wrapper that adds behaviour before/after calling the original). |
| Why use functools.wraps? | Without it the wrapper function masks the original's __name__, __doc__ and other metadata. @functools.wraps(fn) copies those attributes onto the wrapper, so debugging and introspection still see the original name. |
| How do you write a decorator that accepts arguments? | Add an outer layer: a function that takes the arguments and returns the actual decorator. Three levels total - argument receiver -> decorator -> wrapper. |
| Where are decorators used in the standard library? | @property, @classmethod, @staticmethod, @functools.lru_cache, @functools.cache, @dataclasses.dataclass. Also heavily used in frameworks: @app.route in Flask, @pytest.fixture, etc. |
Data model and dunder methods
Python's data model lets user-defined classes hook into built-in syntax. Knowing the key dunder methods signals genuine language depth.
| Dunder | What it enables |
|---|---|
| __repr__ / __str__ | repr() and str() / print(). __repr__ should be unambiguous (ideally eval-able); __str__ is human-readable. If only one is defined, __repr__ is the fallback for both. |
| __eq__ and __hash__ | Equality and use as a dict key or set member. If you override __eq__, Python sets __hash__ to None (making the object unhashable) unless you also define __hash__. |
| __len__ / __getitem__ | len() and square-bracket indexing. Implementing both makes the class iterable via the fallback integer-index protocol. |
| __enter__ / __exit__ | The context-manager protocol - used with the with statement. __exit__ receives exc_type, exc_val, exc_tb and can suppress exceptions by returning True. |
| __slots__ | Not a method but a class variable. Replaces the per-instance __dict__ with a fixed set of slots, reducing memory per instance (important for large numbers of small objects). |
| __call__ | Makes an instance callable like a function. Used for stateful callables, function objects, and some design patterns. |
is vs == and copy semantics
| Question | Sharp answer |
|---|---|
| Difference between is and ==? | is tests identity - both names point to the same object in memory (same id()). == tests equality - calls __eq__. Two distinct objects can be equal without being identical. |
| Why does x = 256; y = 256; x is y return True? | CPython caches small integers (-5 to 256) and short interned strings as singletons. This is an implementation detail of CPython, not guaranteed by the language spec - never rely on it in production code. |
| Shallow copy vs deep copy? | A shallow copy (list.copy(), copy.copy()) creates a new container but the elements are still the same objects. A deep copy (copy.deepcopy()) recursively copies all contained objects. Use deep copy when the structure contains nested mutables you need to isolate. |
| How do you copy a dict? | Shallow: d.copy() or dict(d). Deep: copy.deepcopy(d). For a one-level dict of immutables, shallow is sufficient and faster. |
asyncio basics
asyncio is Python's standard concurrency model for I/O-bound work. Interviewers at companies that use Python backends expect you to explain the event loop and coroutines.
| Question | Sharp answer |
|---|---|
| What is a coroutine? | A function defined with async def. Calling it returns a coroutine object; it does not run until awaited or scheduled on an event loop. The event loop drives coroutines by calling send() on the underlying generator. |
| What does await do? | Suspends the current coroutine and yields control back to the event loop until the awaitable (another coroutine, a Task, or a Future) completes. The event loop can run other coroutines in the meantime. |
| asyncio vs threading for I/O? | asyncio uses cooperative multitasking on a single thread - lower overhead, no race conditions on shared state. Threading uses OS threads - simpler to retrofit into synchronous code but has locking overhead. asyncio scales to thousands of concurrent connections; threading caps out sooner. |
| How do you run multiple coroutines concurrently? | asyncio.gather(*coroutines) runs them concurrently and collects results. asyncio.create_task() schedules a coroutine as a background Task without waiting. TaskGroup (3.11+) adds structured concurrency with automatic cancellation on failure. |
| How do you call a blocking function from async code? | loop.run_in_executor(None, blocking_fn, *args) or asyncio.to_thread(blocking_fn, *args) (3.9+). This offloads the call to a thread-pool executor so the event loop is not blocked. |
For the algorithmic side of Python interviews, see the LeetCode Patterns guide - it covers the data structures and patterns that appear alongside language questions in coding rounds.
Sources & further reading
- 1Python Data Model - language reference — docs.python.org
- 2Glossary: iterator, generator, coroutine — docs.python.org
- 3asyncio - Asynchronous I/O — docs.python.org
- 4PEP 703 - Making the Global Interpreter Lock Optional — python.org