← All languages
Python interview questions
40 curated Python concept questions with model answers. Practice them out loud in the scored Fundamentals room.
Python interviews mix language mechanics with the design decisions that shape production code. Interviewers probe the GIL, decorators, generators, and the classic mutable-default-argument trap because these are the questions that separate someone who writes Python from someone who understands it.
- The GIL and concurrency: when threads help, when they do not, and how multiprocessing or asyncio changes the answer.
- Decorators and context managers: write one that wraps a function, and explain how with and functools.wraps work.
- Mutable vs immutable objects and the default-argument pitfall: why def f(items=[]) shares state across calls.
What interviewers are really testing
Python interviewers are listening for the mental model behind the code: the GIL, name binding, mutability, and the real cost of the abstractions you reach for. Knowing that decorators and generators are just callables and iterators — and being able to write one from scratch — is what separates a confident answer from a memorised one.
Basic
When would you use a list, tuple, set, or dict?
- list: ordered, mutable, allows duplicates — a general sequence.
- tuple: ordered, immutable, hashable when its contents are — good for fixed records and dict keys.
- set: unordered, unique, O(1) membership — deduplication and set math.
- dict: key→value mapping with O(1) average lookup, insertion-ordered since 3.7.
- Pick by the operations you need: indexing (list/tuple), uniqueness (set), or lookup by key (dict).
What does mutable vs immutable mean in Python?
- Immutable objects (int, float, str, tuple, frozenset) cannot change in place; operations return a new object.
- Mutable objects (list, dict, set, bytearray) can change in place, so aliases see the change.
- Assignment never copies; two names can point at the same list.
- Only immutable (hashable) objects can be dict keys or set members.
- The default-argument gotcha comes from mutability: `def f(x=[])` reuses one list across calls — use None and create inside.
What's the difference between `is` and `==`?
- == compares values (uses __eq__); `is` compares identity (same object in memory, uses id()).
- Use `is` for singletons: None, True, False, and sentinel objects.
- Small ints and some strings are interned, so `a is b` can be true by CPython implementation detail — never rely on it for values.
- `is` is faster and cannot be overridden, which is why `x is None` is preferred over `x == None`.
- For containers, == is recursive by value.
When should you use a list comprehension instead of a loop?
- A comprehension builds a new collection declaratively: `[f(x) for x in xs if cond]`.
- They are usually faster and more readable for simple transform/filter pipelines.
- Available as list, set, dict, and generator (parenthesized) forms.
- Avoid them when logic is multi-step or has side effects — a plain loop is clearer.
- A generator expression `(x for x in xs)` is lazy and memory-efficient for large or streaming input.
Explain *args and **kwargs.
- *args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict.
- In a call, * unpacks a sequence and ** unpacks a mapping into arguments.
- They make wrappers and decorators generic without knowing the wrapped signature.
- Ordering in a def: positional, *args, keyword-only, **kwargs.
- Keyword-only parameters (after *) force callers to name them, which improves clarity.
How does the `with` statement work?
- `with` calls __enter__ on the object and guarantees __exit__ runs even if the block raises.
- It is the idiomatic way to manage resources: files, locks, DB connections, transactions.
- __exit__ receives the exception info and can suppress it by returning True.
- contextlib.contextmanager turns a generator function into a context manager (code before yield is __enter__).
- It replaces try/finally boilerplate and makes cleanup reliable.
Walk through try / except / else / finally.
- try holds the risky code; except handles specific exception types in order.
- else runs only if no exception was raised — good for code that should not be guarded.
- finally always runs, even on return or an unhandled exception; used for cleanup.
- Catch the narrowest exception you can; a bare `except:` also swallows KeyboardInterrupt and hides bugs.
- Re-raise with `raise` (no argument) to preserve the traceback when you only wanted to log.
Why is `range` preferred over a list of numbers?
- range is a lazy, memory-constant sequence object — it computes values on demand, not up front.
- range supports the full slicing and membership protocol with O(1) memory.
- range(stop) / range(start, stop) / range(start, stop, step) cover the common loops.
- For iterating values use `for i in range(n)`; when you only need the items, iterate the container directly.
- enumerate gives index and value together, which is cleaner than manual counting.
What are f-strings and why prefer them?
- f-strings embed expressions inside string literals: f"{user.name} has {len(items)} items".
- They are evaluated at runtime, are concise, and are the fastest string formatting option.
- Format specs after a colon control presentation: f"{value:.2f}", f"{n:>8}".
- The `=` specifier (f"{x=}") prints the expression and its value for debugging.
- Prefer them over %-formatting and str.format for readability.
How does slicing with negative indices work?
- s[start:stop:step] returns a new sequence; stop is exclusive.
- Negative indices count from the end: s[-1] is the last element.
- Omitted bounds default to the start/end, so s[::-1] reverses and s[:] shallow-copies.
- Out-of-range slices clamp instead of raising IndexError, unlike direct indexing.
- Slicing a list copies references (shallow); use copy.deepcopy for nested structures.
What do dict.get, setdefault, and update do?
- get(key, default) returns a default instead of raising KeyError when the key is missing.
- setdefault(key, default) returns the existing value or inserts the default and returns it.
- update merges another mapping in place, overwriting existing keys.
- `key in dict` is the idiomatic membership test; d[key] raises when absent.
- dict is insertion-ordered, so iteration order is insertion order.
How do split, join, and strip work?
- split(sep) returns a list of substrings; str.split() with no args splits on any whitespace and drops empties.
- join is the inverse: sep.join(list) builds a string from an iterable of strings.
- strip/lstrip/rstrip remove leading/trailing whitespace or given characters (not substrings).
- Strings are immutable, so these return new strings.
- replace returns a new string and takes an optional count.
How does tuple unpacking work, including starred and swap?
- a, b = (1, 2) assigns by position and raises if the count mismatches.
- a, *rest = [1,2,3] collects remaining items into a list.
- Swapping is a, b = b, a — the right side is evaluated first into a tuple.
- It also works in for loops and function arguments/unpacking.
- The number of targets must match unless you use a starred target.
What are enumerate and zip used for?
- enumerate(iterable, start=0) yields (index, value) pairs, avoiding manual counters.
- zip(*iterables) yields tuples of aligned items and stops at the shortest input.
- zip is lazy in Python 3 and can be reversed with zip(*pairs).
- Both return iterators, so wrap in list() when you need a list.
- itertools.zip_longest fills missing values instead of stopping early.
When is a lambda useful, and how do you sort with a key?
- A lambda is a single-expression anonymous function; it can't contain statements.
- The common use is a short key or callback, e.g. sorted(items, key=lambda x: x.age).
- sorted and list.sort accept a key function and a reverse flag.
- Use operator.itemgetter/attrgetter instead of a lambda when you need speed or clarity.
- Name a real function with def when the logic is more than a trivial expression.
How do any and all work, and why pair them with generators?
- any is True if at least one item is truthy; all is True only if every item is truthy.
- Both short-circuit, so they stop at the first decisive value.
- Passing a generator avoids building an intermediate list.
- all([]) is True and any([]) is False (vacuous truth).
- They are the idiomatic replacement for a loop with a boolean flag.
What is falsy in Python?
- Falsy values: None, False, 0, 0.0, 0j, '' (empty string), and empty containers ([], {}, set(), ()).
- Everything else is truthy, including the string '0' and a non-empty list.
- Custom classes can define __bool__ or __len__ to control truthiness.
- Prefer explicit checks like `if x is not None` when None is distinct from other falsy values.
Explain positional, keyword, and keyword-only arguments.
- Positional arguments are matched left to right; keyword arguments are matched by name.
- Arguments after * are keyword-only; before / are positional-only.
- A call can mix positional then keyword, but not the other way around.
- Default values are evaluated once at definition time (the mutable-default gotcha).
- Keyword arguments make call sites readable and order-independent.
Explain Python's LEGB scope rule and nonlocal/global.
- Names resolve Local -> Enclosing -> Global -> Built-in.
- Assignment inside a function makes a name local unless declared global or nonlocal.
- nonlocal rebinds a name in the nearest enclosing function scope; global targets module scope.
- Reading an enclosing variable is fine without a declaration; rebinding it is not.
- UnboundLocalError happens when you assign to a name before reading it locally.
What are the import styles and what does `if __name__ == '__main__'` do?
- import module, from module import name, and import module as alias are the three styles.
- Importing a module runs it once and caches it in sys.modules.
- The __name__ guard runs a block only when the file is executed directly, not when imported.
- Circular imports are a sign of tangled dependencies and can raise ImportError.
- Relative imports (from . import x) work only inside a package.
Advanced
How do decorators work?
- A decorator is a callable that takes a function and returns a replacement, applied with @ at definition time.
- `@deco` above `def f` is sugar for `f = deco(f)`.
- They wrap extra behavior — logging, timing, caching, auth — around the original function.
- Use functools.wraps to preserve the wrapped function's name, docstring, and metadata.
- Decorators with arguments are factories: the outer function returns the actual decorator.
What is a generator and how does it differ from a normal function?
- A generator function uses yield and returns a lazy iterator; calling it does not run the body.
- State (local variables and position) is suspended between yields, so it can produce large/infinite sequences in constant memory.
- next() resumes until the next yield; StopIteration signals exhaustion.
- Generator expressions `(x for x in xs)` are the concise form.
- Use them for streaming, pipelines, and to avoid building huge intermediate lists.
What is the GIL and how does it affect concurrency?
- The Global Interpreter Lock lets only one thread execute Python bytecode at a time in CPython.
- It makes CPU-bound multithreading ineffective for parallelism; use multiprocessing or native extensions instead.
- It does not block I/O: a thread releases the GIL while waiting on network, disk, or sleep, so threads still help I/O-bound work.
- asyncio sidesteps threads entirely with a single-threaded event loop for high-concurrency I/O.
- Free-threaded/no-GIL builds exist (3.13+) but are experimental and not the default.
How does asyncio work and when is it the right tool?
- asyncio runs a single-threaded event loop that multiplexes many I/O operations via coroutines (async/await).
- await yields control to the loop while waiting, letting other tasks run — no threads required.
- asyncio.gather runs awaitables concurrently; asyncio.create_task schedules one in the background.
- It shines for high-concurrency network I/O; it does not speed up CPU-bound code.
- Blocking calls (time.sleep, requests, heavy CPU) must be offloaded with run_in_executor or they stall the loop.
How does Python resolve methods with multiple inheritance (MRO)?
- Method resolution order is the linearized order in which base classes are searched, computed by the C3 algorithm.
- TypeError at class creation if the hierarchy is inconsistent (no valid linearization).
- super() follows the MRO, not just the immediate parent — critical for cooperative multiple inheritance.
- Inspect it with ClassName.__mro__ or ClassName.mro().
- Prefer composition or mixins over deep diamond hierarchies.
Explain closures and the late-binding loop gotcha.
- A closure captures variables by reference from the enclosing scope, not their value at definition time.
- Lambdas/functions created in a loop all see the loop variable's final value — the classic late-binding bug.
- Fix it by binding the value as a default argument (`lambda x=i: …`) or with functools.partial.
- Closures read enclosing variables; to rebind one, declare nonlocal and, for module scope, global.
- Each call to the outer function creates a fresh scope, so closures over different calls are independent.
What is a descriptor and how does @property relate to it?
- A descriptor is an object defining __get__/__set__/__delete__ that customizes attribute access on a class.
- Functions are descriptors, which is how methods bind self.
- property is a built-in descriptor that maps attribute access to getter/setter/deleter methods.
- @property gives computed or validated attributes that look like plain fields.
- classmethod and staticmethod are also implemented via descriptors.
What does __slots__ do?
- __slots__ declares a fixed set of instance attributes, replacing the per-instance __dict__.
- It reduces memory per object and speeds up attribute access for classes with many instances.
- Trade-offs: you cannot add attributes outside the slot list, and multiple inheritance with non-empty slots can conflict.
- Properties conflict with slots of the same name.
- Worth it for millions of small objects; unnecessary for typical app classes.
What's the difference between shallow and deep copy?
- copy.copy / list[:] / dict() create a shallow copy: top-level is new, nested objects are shared.
- copy.deepcopy recursively copies nested objects, so mutating the copy cannot affect the original.
- Assignment (=) copies neither — it aliases the same object.
- deepcopy is slower and must handle cycles (it tracks memo) and uncopyable objects.
- Choose shallow when nested data is treated as immutable; deep when it is mutated independently.
How does functools.lru_cache work and what are its caveats?
- It memoizes a function's results keyed by the arguments, turning repeated calls into O(1) lookups.
- Set maxsize=None for an unbounded cache or a number for LRU eviction; typed=True separates args of different types.
- Arguments must be hashable — no lists or dicts.
- It returns the same cached object, so mutating a returned mutable result corrupts the cache.
- For async functions use an async-aware cache or cache the awaited value explicitly; also expose cache_clear for invalidation.
What is a metaclass and when would you use one?
- A metaclass is the class of a class; type is the default, so classes are instances of their metaclass.
- It can intercept class creation via __new__/__init__ to register classes, enforce rules, or inject methods.
- Use it sparingly — most needs are met by inheritance, __init_subclass__, or decorators.
- ABCMeta is the built-in example, powering abstractmethod enforcement.
- metaclass=... in a class definition sets it.
How do abstract base classes work?
- Subclass abc.ABC and decorate methods with @abstractmethod to require implementations.
- Instantiating a subclass that misses an abstract method raises TypeError.
- ABCs can also be used as virtual base classes via register().
- They document and enforce an interface without the strictness of Java.
- Protocols (typing.Protocol) offer structural typing as an alternative.
What does @dataclass generate, and what are its pitfalls?
- __init__, __repr__, and __eq__ are generated from the fields.
- field(default_factory=list) avoids the shared-mutable-default bug.
- frozen=True makes instances immutable and hashable; slots=True saves memory.
- Ordering requires order=True; comparison is field order.
- Default values are evaluated once, so mutable defaults need default_factory.
How do generator send/throw/close work?
- gen.send(value) resumes the generator and makes value the result of the paused yield.
- The first send must be None (or use next()) to run up to the first yield.
- gen.throw(exc) raises inside the generator at the yield point.
- gen.close() raises GeneratorExit inside the generator, running finally blocks.
- This is the basis of coroutine-like pipelines before async/await.
What are async generators and async comprehensions?
- An async generator uses `async def` with `yield` and is consumed with `async for`.
- They combine streaming with awaiting I/O per item — good for paginated APIs and websockets.
- Async comprehensions use async for and optionally await inside.
- They must run inside an event loop (not a plain for loop).
- anext() and aclose() are the async analogs of next() and close().
How does CPython allocate memory for small and large objects?
- CPython uses a private allocator with arenas of 256 KB split into pools and 512-byte blocks.
- Objects of 512 bytes or less go through pymalloc; larger ones use malloc directly.
- Reference counting frees objects immediately; a cyclic GC handles reference cycles.
- Small ints (-5 to 256) are cached, and interned strings/identifiers are shared.
- id() exposes the address, which is why it differs between equal-but-distinct objects.
What is monkey patching and why is it risky?
- Monkey patching replaces attributes or methods at runtime on a module or class.
- It is used by test mocks and libraries like gevent, but action-at-a-distance is hard to debug.
- Patching a method on a class changes it for every instance and caller.
- Prefer dependency injection or subclassing; confine patching to tests with unittest.mock.patch.
- Patching a module doesn't update references already imported elsewhere.
What is singledispatch used for?
- functools.singledispatch turns a function into a generic function that dispatches on the type of the first argument.
- Register implementations with @fn.register(SomeType).
- It is a clean alternative to isinstance chains and keeps behavior near its type.
- The base implementation is the fallback when no registered type matches.
- Use functools.singledispatchmethod for methods.
What are the useful contextlib tools?
- contextmanager turns a generator into a context manager (before yield is __enter__, after is __exit__).
- suppress swallows listed exceptions for a block.
- ExitStack manages a dynamic set of context managers and callbacks.
- nullcontext is a no-op context manager useful in conditional code.
- closing ensures .close() is called on an object.
When do you pick threads, processes, or asyncio?
- I/O-bound work: threads or asyncio (asyncio for many concurrent sockets).
- CPU-bound work: multiprocessing (or native extensions), because the GIL serializes bytecode.
- Threads are simpler for blocking libraries that have no async client.
- asyncio needs an async ecosystem; mixing blocking calls stalls the loop.
- Processes have higher memory/startup cost and require pickled data between them.
Common mistakes
- Using a mutable default argument such as def f(items=[]) and then being surprised that state persists across calls.
- Claiming threads give CPU parallelism without mentioning the GIL, or that asyncio makes CPU-bound work concurrent.
- Mutating a list while iterating it, or assuming list and tuple differ only in punctuation.
- Confusing is with ==, or relying on small-integer caching for identity checks.
How to rehearse these answers
- Write a decorator that times a function and one that retries it, then explain the role of functools.wraps.
- Convert a loop into a generator and reason about the memory difference on a large range.
- Reproduce the mutable default trap, fix it with None, and explain why the fix works.
- Sketch a producer and consumer over asyncio.Queue and state exactly where the event loop yields.
New to technical interviews? Read how to rehearse coding and system design.
Other languages
JavaScript interview questions · Java interview questions · C++ interview questions · C# interview questions · Go interview questions