The questions that come up most often, each with a sample answer you can adapt into your own words. Read them out loud until the explanation feels natural.
What is the difference between a list and a tuple, and when would you use each?
FundamentalsA list is mutable and a tuple is immutable. Use a tuple when the collection is a fixed record (a coordinate, a return of multiple values) or when you need a hashable sequence (tuples can be dict keys or set members; lists cannot). Use a list when the collection grows, shrinks, or is reordered. The immutability of tuples also makes intent clearer to the next reader.
Explain the Global Interpreter Lock (GIL) and its impact on concurrency.
AdvancedThe GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time. It means multithreading does not give you parallel speedup for CPU-bound work; the threads just take turns. It does help I/O-bound work, because the GIL is released during blocking I/O. For CPU-bound parallelism you use the multiprocessing module (separate processes, separate GILs) or push the heavy loop into NumPy or a C extension that releases the GIL.
What does a generator do, and why use one over a list?
IntermediateA generator produces values lazily, one at a time, using yield, and holds only the current state in memory rather than the whole sequence. You use one when the sequence is large or infinite, or when the consumer may stop early, because you avoid materialising everything up front. The tradeoff is that a generator is single-pass and not indexable.
Why is using a mutable default argument a bug?
IntermediateDefault arguments are evaluated once, when the function is defined, not on each call. So def f(x, acc=[]) shares the same list across every call that omits acc, and mutations leak between calls. The fix is to default to None and create a fresh list inside the function: acc = acc if acc is not None else [].
How does Python manage memory?
AdvancedCPython uses reference counting as the primary mechanism: each object tracks how many references point to it, and it is freed when that count hits zero. Because reference counting alone cannot reclaim reference cycles, a generational cyclic garbage collector periodically finds and frees unreachable cycles. Most objects die by reference counting; the GC handles the cyclic minority.
What is the difference between deep copy and shallow copy?
IntermediateA shallow copy (copy.copy or list slicing) duplicates the outer container but shares the inner objects, so mutating a nested object affects both copies. A deep copy (copy.deepcopy) recursively duplicates everything, so the two are fully independent. Reach for deepcopy only when you actually need isolation, since it is more expensive.
What is a context manager and when would you write one?
IntermediateA context manager defines setup and teardown around a block via __enter__ and __exit__ (or the @contextmanager decorator), and is used with the with statement. The canonical case is resource management: open files, acquire locks, or begin database transactions so they are always cleaned up even if the block raises. It replaces error-prone try/finally boilerplate.
How do *args and **kwargs work?
Fundamentals*args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dict. They let a function accept a variable number of arguments and are commonly used to forward arguments transparently to a wrapped function, for example in decorators.
How do decorators work?
IntermediateA decorator is a callable that takes a function and returns a replacement, applied with the @ syntax. It wraps behaviour around the original function without editing it: logging, caching, access checks, retries. The wrapper usually accepts *args and **kwargs so it forwards any signature, and functools.wraps preserves the original name and docstring. Interviewers often ask you to write one on the spot, so it is worth having the three-layer shape memorised: the decorator, the wrapper, and the call through to the wrapped function.
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def greet(name):
return f"hello {name}"
What is the difference between __str__ and __repr__?
Fundamentals__str__ produces the readable, user-facing form of an object and is what print and str() call. __repr__ produces the unambiguous, developer-facing form, ideally something you could paste back into a session to recreate the object, and it is what the interactive prompt and containers use. If you define only one, define __repr__, because str() falls back to it. In practice a good __repr__ pays for itself the first time you inspect a list of your objects in a debugger.
How would you sort a list of dicts by one of their values?
IntermediateUse sorted with a key function: sorted(rows, key=lambda r: r['age']) returns a new sorted list, while rows.sort(...) sorts in place. operator.itemgetter('age') is the slightly faster, more idiomatic key for this exact case. For multi-field sorts, return a tuple from the key, and use reverse=True (or negate numeric fields) for descending order. Python's sort is stable, so sorting twice by different keys layers the orderings, a neat property interviewers like to hear mentioned.
from operator import itemgetter
rows = [
{"name": "Ada", "age": 36},
{"name": "Grace", "age": 45},
]
by_age = sorted(rows, key=itemgetter("age"))
newest_first = sorted(rows, key=itemgetter("age"), reverse=True)
When would you choose asyncio over threads?
AdvancedBoth suit I/O-bound work, since neither is blocked by the GIL during I/O waits. asyncio shines when you have very many concurrent connections (thousands of sockets or HTTP calls), because coroutines are cheaper than threads and switching happens explicitly at each await, which removes most race conditions. Threads are simpler when you have a modest number of blocking calls or must use libraries that are not async-aware. Neither helps CPU-bound work; that is what multiprocessing is for. Giving this decision tree crisply is a strong senior signal.