Python backend interview prep

Python Interview Questions and Answers 2026

A focused set of Python interview questions for backend developers. Use each answer as a spoken structure: start with the concept, add a concrete example, then mention trade-offs.

Core Python Data Model

1What built-in data types does Python have, and which are mutable?

Python has scalar types such as NoneType, bool, int, float and complex; text and binary types such as str, bytes and bytearray; sequence types such as list, tuple and range; and collection types such as dict, set and frozenset. The important interview distinction is mutability: list, dict, set and bytearray can change in place, while numbers, bool, str, bytes, tuple, range, frozenset and None are immutable. A useful nuance is that a tuple is immutable as a container, but it can still contain references to mutable objects.

2How do list, tuple, set and dict differ in real code?

A list is an ordered mutable sequence, so it is good for queues of work, collected results and ordered data that changes. A tuple is ordered but immutable, so it is useful for fixed records, function returns and values that should not be reassigned. A set stores unique hashable objects and is ideal for membership checks or deduplication. A dict maps hashable keys to values and is the normal structure for indexes, lookups and JSON-like data. In modern Python, dict preserves insertion order, but its main purpose is still key-based access.

3What is the difference between is and ==?

== asks whether two objects are equal by value, usually through __eq__. is asks whether two references point to the exact same object in memory. In production code, is should usually be reserved for singleton checks such as value is None, value is True only when identity really matters, or sentinel objects. Do not rely on implementation details such as small integer caching or string interning; those can make is appear to work in examples while being the wrong semantic choice.

4What is a hashable object and why does it matter?

A hashable object has a hash value that remains stable during its lifetime and equality behavior consistent with that hash. Hashability matters because dict keys and set elements depend on it. Most immutable built-ins are hashable, but not all objects inside an immutable container automatically become hashable: a tuple containing a list is not hashable. In interviews, connect this to design: if an object is used as a key, its identity-defining fields should not change after insertion.

5How does slicing work, including negative indexes and steps?

A slice sequence[start:stop:step] includes start, excludes stop and moves by step. Missing boundaries are inferred from the direction of the step. Negative indexes count from the end, so s[-1] is the last item. A negative step walks from right to left; s[::-1] creates a reversed sequence for many sequence types. The trade-off is that slicing often creates a new object, so it is expressive but not always free for large data.

Functions and Scope

6How does Python pass arguments to functions?

Python passes object references by assignment, often called call by sharing. The function receives local parameter names bound to the same objects supplied by the caller. If the object is mutable, the function can mutate it and the caller will observe the change. If the function reassigns the parameter name, only the local binding changes. A strong interview answer includes both examples: appending to a passed list mutates it, while assigning items = [] inside the function does not replace the caller's list.

7Why are mutable default arguments dangerous?

Default argument values are evaluated once when the function is defined, not each time the function is called. If a default is a list or dict, all calls can share the same object and accidentally leak state. The safe pattern is def f(items=None): then create items = [] inside when items is None. There are rare cases where shared state is intentional, such as a small cache, but then it should be explicit and documented because it surprises readers.

8What are *args and **kwargs used for?

*args collects extra positional arguments into a tuple, while **kwargs collects extra keyword arguments into a dict. They are useful for wrappers, decorators, flexible APIs and forwarding calls. At the call site, *iterable expands positional values and **mapping expands keyword values. The trade-off is clarity: explicit parameters are better when the function has a stable public contract, while args and kwargs are better at boundaries where forwarding or compatibility matters.

9How does Python resolve variable names with LEGB?

Python looks for names in Local, Enclosing, Global and Builtins scopes. Reading a name follows that order. Assigning a name inside a function normally creates a local binding, which is why code can raise UnboundLocalError if it reads a name before local assignment. global redirects assignment to module scope, and nonlocal redirects assignment to an enclosing function scope. In good production code, use both sparingly because they make state flow less obvious.

10What is a closure and what is late binding?

A closure is a function that keeps access to variables from an enclosing scope after that outer function has returned. This is useful for decorators, callback factories and configured functions. Late binding means the free variable is looked up when the inner function is called, not when it is created. Lambdas created in a loop can all see the final loop value. Capture the current value with a default argument, for example lambda i=i: i, or use a small factory function.

OOP, Iteration and Resource Management

11How do class variables differ from instance variables?

A class variable is stored on the class object and shared by instances unless an instance shadows it with its own attribute. An instance variable is stored on a particular object, usually assigned as self.name in __init__. Immutable class constants are usually fine, but mutable class attributes are a common bug when they are accidentally used as per-instance state. In an interview, mention the lookup order: Python checks the instance first, then the class and its ancestors.

12How do inheritance, MRO and super() work?

Python resolves attributes through the Method Resolution Order, which is visible with ClassName.__mro__. In single inheritance it is straightforward, but in multiple inheritance Python uses C3 linearization to create a consistent order. super() does not mean 'call my parent' exactly; it means 'call the next implementation in the MRO'. That makes cooperative multiple inheritance possible when methods accept compatible arguments and each class calls super() correctly.

13What is the difference between iterable, iterator and generator?

An iterable is any object that can return an iterator from iter(), such as a list or a file object. An iterator is the stateful object that implements __next__ and raises StopIteration when exhausted. A generator is a convenient way to create an iterator with yield or a generator expression. Generators are memory-efficient because they produce values lazily, but they are usually one-shot and cannot be indexed like a list.

14What is a context manager and why is with useful?

A context manager defines enter and exit behavior with __enter__ and __exit__, or can be created with contextlib.contextmanager. The with statement makes resource management explicit and reliable: files are closed, locks are released and transactions can commit or roll back even when exceptions happen. __exit__ receives exception information and may suppress the exception by returning true, though suppressing errors should be used carefully.

15How do shallow copy and deep copy differ?

A shallow copy creates a new outer container but keeps references to the same nested objects. A deep copy recursively copies nested objects when possible. For simple flat lists, a shallow copy is often enough. For nested mutable structures, shallow copy can still share state and surprise you. Deep copy is more expensive and may be incorrect for resources such as open files, sockets, database sessions or objects with identity-sensitive behavior.

Runtime, Async and Production Code

16What is the GIL and how does it affect backend code?

In standard CPython, the Global Interpreter Lock allows only one thread to execute Python bytecode at a time. This means threads do not usually speed up CPU-bound Python code. Threads are still useful for I/O-bound work because the GIL is released while waiting on many blocking operations. For CPU-heavy work, use multiprocessing, native extensions, vectorized native libraries, separate services or newer free-threaded builds only after checking compatibility. A backend answer should connect the GIL to workload type, not treat it as 'threads are useless'.

17When should you choose threads, processes or asyncio?

Use threads when you have blocking I/O or libraries that expose only synchronous APIs. Use processes for CPU-bound work, isolation or crash containment, accepting serialization and memory overhead. Use asyncio when you have many concurrent I/O operations and async-compatible libraries, such as async database drivers or HTTP clients. The practical choice also depends on your framework and dependencies: mixing async code with blocking calls can make performance worse.

18What is async/await good for in Python web services?

async and await enable cooperative concurrency. A coroutine gives control back to the event loop when it awaits I/O, allowing other tasks to run. This is valuable for APIs that wait on databases, HTTP calls, queues or sockets. It does not magically make CPU-bound code faster; CPU-heavy work blocks the event loop unless moved to a worker, process or native code. In FastAPI or similar frameworks, async endpoints help only when the whole call path avoids blocking operations.

19How should exceptions be handled in production Python?

Catch the most specific exception you can handle, and catch it at a layer that can recover, add context or translate it into a domain error. Avoid bare except and avoid silently swallowing errors because that hides data loss and operational problems. Use raise to re-raise the same exception and raise NewError(...) from exc to preserve causal context. Good production code also logs at boundaries, returns appropriate HTTP errors and keeps exception messages useful without leaking secrets.

20How would you structure and test a production Python service?

Separate domain logic from framework and infrastructure code so core behavior can be tested without a server or database. Use configuration from environment or typed settings, logging instead of print, dependency injection at boundaries, and explicit timeouts for network calls. For tests, combine unit tests for pure logic, integration tests for database or external boundaries and a few end-to-end checks for critical flows. Mock external systems at clear boundaries, but test observable behavior rather than private implementation details.