Skip to main content

Python Interview Questions (2026)

100 real interview questions with in-depth answers — 30 basic, 40 intermediate, 30 advanced. Updated April 2026.

Preparing for a Python Developer role?

Python is a high-level, interpreted, dynamically typed programming language known for readable syntax and a huge standard library. Key features include duck typing, first-class functions, automatic memory management with reference counting plus a cyclic garbage collector, and strong support for both OOP and functional patterns. It is popular in data, web, scripting, and ML because of libraries like NumPy, Pandas, Django, and PyTorch.

Lists are mutable and use square brackets; tuples are immutable and use parentheses. Tuples are slightly faster and can be used as dict keys because they are hashable, while lists cannot.

python
lst = [1, 2, 3]; lst.append(4)  # OK
tup = (1, 2, 3); tup[0] = 9       # TypeError

CPython uses reference counting as its primary mechanism: every object tracks how many references point to it, and it is freed when the count hits zero. A generational garbage collector handles reference cycles. Small integers and short strings are interned and reused to reduce allocations.

PEP 8 is the official Python style guide covering naming, indentation (4 spaces), line length (79/99 chars), imports ordering, and whitespace. Following it makes code predictable across teams. Tools like ruff, black, and flake8 enforce it automatically.

`==` compares values using `__eq__`; `is` compares identity (same object in memory). `a == b` can be true even when `a is b` is false. Use `is` only for singletons like `None`, `True`, `False`.

`*args` collects positional arguments into a tuple, `**kwargs` collects keyword arguments into a dict. They allow flexible function signatures and forwarding. ```python def f(*args, **kwargs): print(args, kwargs) f(1, 2, x=3) # (1, 2) {"x": 3} ```
A concise way to build a list from an iterable with optional filtering. It is usually faster and more readable than an explicit loop. ```python squares = [x*x for x in range(10) if x % 2 == 0] ```
Immutable objects (int, float, str, tuple, frozenset) cannot change after creation; any "change" returns a new object. Mutable objects (list, dict, set, most user classes) can be modified in place. Immutable types are hashable, which matters for dict keys and set members.
`copy.copy` creates a shallow copy — the outer container is new but nested objects are shared. `copy.deepcopy` recursively copies all nested objects so nothing is shared. Use deep copy when mutating nested state would corrupt the original.
`__init__` is the initializer called after an instance is created by `__new__`. It sets up instance attributes. It is not a constructor in the C++ sense — the object already exists when `__init__` runs.
Use the `open()` builtin inside a `with` block so the file closes automatically. Modes: `r` read, `w` write (truncate), `a` append, `rb`/`wb` binary. ```python with open("data.txt") as f: for line in f: print(line.rstrip()) ```
In Python 3 there is only `range`, and it is a lazy sequence object (not a list). In Python 2, `range` built a list and `xrange` was the lazy version. Python 3 unified the behavior under `range`.
A single-expression anonymous function. Useful as a short callback passed to `sorted`, `map`, or `filter`. Prefer `def` when the logic is more than a single expression. ```python sorted(users, key=lambda u: u.last_name) ```
A decorator is a callable that wraps another callable to add behavior — logging, caching, auth — without modifying its source. They are applied with `@decorator` above the `def`. ```python def log(fn): def wrap(*a, **kw): print(fn.__name__) return fn(*a, **kw) return wrap ```
`self` is the conventional name for the instance passed as the first argument to instance methods. Python passes it automatically when you call `obj.method()`. You can name it anything, but `self` is expected by PEP 8.
It distinguishes whether a file is being run directly or imported. Code under that guard runs only when the script is the entry point, so importing the module does not trigger side effects.
Use `try/except/else/finally`. Catch specific exceptions, not a bare `except`. Use `else` for code that should run only if no exception was raised, and `finally` for cleanup. ```python try: x = int(s) except ValueError: x = 0 finally: print("done") ```
A module is a single `.py` file. A package is a directory with an `__init__.py` (or a namespace package) that can contain multiple modules. Packages allow hierarchical organization with dotted imports like `mypkg.utils.io`.
Sets store unique, hashable elements and support O(1) membership tests, union, intersection, and difference. They are useful for deduplication and set math. Frozensets are immutable and hashable.
Slicing uses `seq[start:stop:step]` to extract a subrange. Negative indices count from the end. Slicing a list returns a new list; slicing a string returns a new string. ```python [1,2,3,4,5][1:4:2] # [2, 4] ```
`append` adds one item to the end. `extend` iterates its argument and adds each item. `lst.append([1,2])` gives a nested list; `lst.extend([1,2])` flattens.
F-strings (PEP 498) interpolate expressions directly: `f"{name=}, total={total:.2f}"`. They are faster than `%` formatting or `.format()` and support expressions, format specs, and debug `=`.
`@classmethod` receives the class (`cls`) as its first arg and can be used as an alternative constructor. `@staticmethod` receives nothing automatic and is just a regular function namespaced on the class. Use classmethod when you need the class; staticmethod when you just want grouping.
`None` is the singleton of `NoneType`, representing the absence of a value. Compare with `is None`, not `== None`. Functions without an explicit `return` return `None`.
A hash map mapping hashable keys to arbitrary values. Average O(1) lookup, insertion, and deletion. Since Python 3.7, dicts preserve insertion order.
The walrus `:=` (PEP 572) assigns a value as part of an expression. Handy in comprehensions and loops to avoid re-computing. ```python while (chunk := f.read(1024)): process(chunk) ```
Optional static type annotations introduced in PEP 484. They are not enforced at runtime but are checked by tools like mypy/pyright. They improve IDE support and catch bugs early. ```python def greet(name: str) -> str: ... ```
An object's suitability is determined by the presence of methods/attributes, not its class. "If it quacks like a duck..." Pythonic code relies on duck typing rather than explicit type checks.
Both compare element-wise. `[1,2] == (1,2)` is False because the types differ even though contents match. Sequence equality in Python requires the same type.
Use `pip install <pkg>` inside a virtual environment. For reproducible builds, pin versions in `requirements.txt` or use `pyproject.toml` with Poetry/uv/pip-tools. `pipx` is for installing CLI tools.
CPython's GIL is a mutex that allows only one thread to execute Python bytecode at a time. It simplifies memory management but prevents true parallel CPU-bound work with threads. Use `multiprocessing` or native extensions (NumPy releases the GIL) for CPU-bound parallelism; threads still help for I/O-bound work. Python 3.13 introduces an experimental no-GIL build.
Generators are iterators produced by functions that `yield`. They suspend state between calls and are memory-efficient for streams or large sequences. A generator expression `(x*x for x in xs)` is a compact form. ```python def squares(n): for i in range(n): yield i*i ```
Any object implementing `__enter__` and `__exit__` can be used in a `with` statement. `__exit__` runs on both normal and exceptional exit, letting you guarantee cleanup. `contextlib.contextmanager` turns a generator into a context manager.
`__repr__` is for developers and should ideally be unambiguous — often a reproducible expression. `__str__` is for end users and is friendlier. If only one is defined, define `__repr__` because it is used as a fallback for `str`.
An iterable is any object with `__iter__`; calling it returns an iterator. An iterator has `__next__` and raises `StopIteration` at the end. Lists are iterables but not iterators; generators are both.
`super()` walks the MRO (Method Resolution Order), computed by C3 linearization. This guarantees a consistent, single path through the class hierarchy. Always call `super().__init__()` in cooperative multiple inheritance.
A metaclass is the class of a class. `type` is the default metaclass. Custom metaclasses customize class creation — registering subclasses, enforcing interfaces, or modifying attributes. Most use cases are better solved with `__init_subclass__` or decorators.
`asyncio` provides single-threaded concurrency via an event loop. `async def` defines coroutines; `await` suspends until an awaitable completes. Use `asyncio.gather` for concurrent tasks and `asyncio.run` as the entry point. ```python async def main(): results = await asyncio.gather(fetch(a), fetch(b)) ```
`@dataclass` auto-generates `__init__`, `__repr__`, and `__eq__` from annotated attributes. Use `frozen=True` for immutability and `field(default_factory=list)` for mutable defaults. Less boilerplate than manual classes.
Descriptors are objects with `__get__`, `__set__`, or `__delete__`. They customize attribute access. `property`, classmethod, and staticmethod are all implemented as descriptors. They enable ORMs and validation frameworks.
Dynamically replacing attributes or methods at runtime. Useful for testing (patching third-party functions) but dangerous in production because it makes code hard to reason about. Prefer dependency injection.
`abc.ABC` and `@abstractmethod` define interfaces that subclasses must implement. Trying to instantiate an ABC with unimplemented abstract methods raises `TypeError`. They formalize "is-a" relationships.
A decorator that memoizes function calls using a least-recently-used cache. Arguments must be hashable. It is thread-safe and can dramatically speed up recursive or repeated-call code. ```python @lru_cache(maxsize=1024) def fib(n): return n if n < 2 else fib(n-1) + fib(n-2) ```
`weakref` creates references that do not increment the refcount, so the target can still be collected. Used for caches and observer patterns to avoid leaks.
It provides specialized containers: `deque` for O(1) appends/pops at both ends, `Counter` for frequency counts, `defaultdict` for auto-initializing values, `OrderedDict` for order-specific APIs, and `namedtuple` for lightweight records.
`pickle` serializes Python objects to bytes. It is Python-specific and can execute arbitrary code on load, so never unpickle untrusted data. Use JSON or protobuf for cross-language or untrusted contexts.
Threads share memory but are bottlenecked by the GIL for CPU work. Processes have separate memory and bypass the GIL, at the cost of higher startup overhead and IPC complexity. Rule of thumb: threads for I/O, processes for CPU.
Use `cProfile` for a function-level profile, `timeit` for microbenchmarks, and `line_profiler` for per-line timing. `py-spy` and `scalene` are great for sampling production processes without modifying code.
Python uses `==` which calls `__eq__`. For nested mutable structures, equality checks propagate recursively. Two separate lists with the same contents are equal but not identical.
`@property` turns a method into a read-only attribute. You can add `@x.setter` and `@x.deleter` to allow writes and deletes. Useful for computed attributes, validation, and backward-compatible refactors.
Passing collaborators into a function/class instead of constructing them inside. It decouples modules, eases testing (inject mocks), and keeps responsibilities single. Python's first-class functions make DI lightweight.
`venv` creates an isolated Python interpreter directory with its own `site-packages`, so each project has its own dependencies. Activation puts its `bin` directory first on PATH. Tools like uv, poetry, and pipenv build on this.
Method Resolution Order — the sequence Python searches for attributes on a class. Computed using C3 linearization. Inspect it with `ClassName.__mro__`. It ensures consistent behavior in diamond inheritance.
Defining `__slots__` prevents creation of `__dict__` per instance and restricts attributes to the declared set. Saves memory for many instances and slightly speeds attribute access. Incompatible with multiple inheritance in some cases.
`[x for x in it]` materializes a full list; `(x for x in it)` produces a lazy iterator. Use generators for large or unbounded streams and when you iterate once.
`yield from iterable` delegates iteration to another generator, forwarding values and propagating `send`, `throw`, and return values. Cleaner than manual for-loops for generator composition.
Python 3.8 added `f"{var=}"` which expands to `var=<repr>`. Great for quick prints while debugging without retyping names.
Define `__iter__` (return self) and `__next__` (return next value or raise `StopIteration`). Usually easier to write a generator function instead.
A context manager that swallows specified exceptions. Cleaner than empty `except` blocks for ignoring known-harmless errors. ```python with suppress(FileNotFoundError): os.remove(path) ```
Both implement `concurrent.futures.Executor`. Thread pool is best for I/O-bound tasks; process pool bypasses the GIL for CPU-bound work. Use `submit` for individual futures or `map` for parallel iteration.
Use `tracemalloc` to snapshot allocations over time and diff. `objgraph` shows reference chains keeping objects alive. Common causes: module-level caches, closures capturing large objects, unreleased file handles.
A callable defined with `async def` that returns a coroutine object. It executes only when awaited or scheduled in an event loop. Coroutines enable structured concurrency without threads.
Python ints are arbitrary precision — they grow as needed and never overflow. This is slower than fixed-width ints but safer. For tight numerics use NumPy dtypes.
`json` is text, language-neutral, limited to primitive types, and safe. `pickle` is binary, Python-specific, handles arbitrary objects, and is unsafe on untrusted input. Choose based on interoperability needs.
Introduced in PEP 544, protocols enable structural subtyping: a class satisfies a protocol if it has the right methods, no explicit inheritance needed. Useful for duck-typed APIs with static guarantees.
Move the import inside a function, restructure modules to break the cycle, or import just the module rather than specific names. Circular imports often indicate a layering problem.
High-performance iterator building blocks: `chain`, `product`, `combinations`, `permutations`, `groupby`, `islice`, `tee`. They compose into expressive pipelines with low memory footprint.
Define dunder methods like `__add__`, `__eq__`, `__lt__`, `__getitem__` to make custom classes work with built-in operators and functions. `functools.total_ordering` fills in comparison methods from a couple.
Use `/` to mark parameters that must be positional, `*` to mark those that must be keyword. ```python def f(a, b, /, c, *, d): ... ``` Helps API design and prevents breaking changes.
Easiest: module-level globals (modules are already singletons). Alternatives: override `__new__` to return a cached instance, or use a metaclass. Usually a code smell — prefer dependency injection.
Use `@contextlib.contextmanager`; yield once between setup and teardown. Exceptions propagate through the yield and can be caught. ```python @contextmanager def timing(label): t = time.perf_counter() try: yield finally: print(label, time.perf_counter() - t) ```
CPython's gc module tracks container objects in generations. Periodically, it detects cycles by subtracting internal references from refcounts; anything still unreachable is collected. You can tune thresholds with `gc.set_threshold` or disable gc in tight hot loops.
Wrap an `OrderedDict` with a `threading.Lock`. On get, move the key to the end; on put, insert and `popitem(last=False)` if over capacity. `functools.lru_cache` is already thread-safe but not pluggable. For better concurrency, shard the cache by key hash.
`__new__` allocates and returns the new instance and runs before `__init__`. Override it for immutable types (subclassing `tuple`, `str`) or singletons. `__init__` only initializes an already-created object and must return `None`.
`gather` returns results in input order, cancels siblings on error (by default), and is higher level. `wait` takes a set and returns (`done`, `pending`) — useful for timeouts or waiting for first completion. `TaskGroup` (3.11+) is preferred for structured concurrency.
Python walks `sys.path`, consulting meta-path finders and loaders. The `importlib` machinery creates a module object, executes it in that namespace, and stores it in `sys.modules` so re-imports are cached. You can customize via import hooks.
Use `py-spy` for low-overhead sampling, or middleware to log per-request timings with `time.perf_counter`. For memory, `scalene` or `tracemalloc` snapshots. Wire OpenTelemetry to a tracing backend for distributed profiling.
`__getattribute__` is called for every attribute access and must be used carefully to avoid infinite recursion. `__getattr__` is called only when normal lookup fails — safer for proxies and lazy loading.
Maintain a deque of call timestamps; on each call drop timestamps older than the window, check count, and sleep if over. For distributed rate limits, use Redis with a token bucket or fixed window counter. ```python def rate_limit(n, per): calls = collections.deque() def deco(fn): def wrap(*a, **kw): now = time.time() while calls and now - calls[0] > per: calls.popleft() if len(calls) >= n: time.sleep(per - (now - calls[0])) calls.append(time.time()) return fn(*a, **kw) return wrap return deco ```
Options: `multiprocessing` or `concurrent.futures.ProcessPoolExecutor`, offload to C extensions (NumPy, Cython) that release the GIL, use subinterpreters (PEP 554), or adopt Python 3.13's free-threaded build. For very hot loops, Rust via PyO3 is popular.
Async coroutines generalize generators: `await x` is like `yield from` but cooperates with the event loop. Under the hood, coroutines are resumable state machines; PEP 492 introduced the `async`/`await` syntax that replaced the earlier `@asyncio.coroutine` pattern.
Use entry points defined in `pyproject.toml` and load with `importlib.metadata.entry_points`. Or define a base class plus a discovery step that imports modules from a plugins directory. Avoid unrestricted `exec` of user code.
Flask is minimal and flexible; you assemble your stack. Django is batteries-included (ORM, admin, auth) — fastest when you need standard features. FastAPI is async-first, built on Starlette + Pydantic, and great for typed APIs. For I/O-heavy modern services, FastAPI wins on performance.
A hook called when a class is subclassed. Lets parents register or validate children without metaclasses. Much simpler than writing a custom metaclass for plugin registration.
Create `pyproject.toml` with build backend (hatchling, setuptools, poetry-core). Build with `python -m build`, then upload with `twine upload dist/*`. Pin runtime deps loosely, test deps strictly. Cut a tagged release in git for traceability.
Functions defined with `async def` that use `yield`. Iterate with `async for`. Useful for streaming async data sources like paginated APIs.
Wrap the cache with a timestamp per key. On lookup, evict if expired and recompute. `cachetools.TTLCache` does this out of the box with thread safety.
`functools.cache` (3.9+) is an unbounded `lru_cache(maxsize=None)`. Use `cache` for pure functions with bounded inputs; `lru_cache` when you need eviction.
PEP 703 proposes removing the GIL, allowing true multi-threaded execution of Python bytecode. It requires changes to reference counting (biased refcount) and container thread safety. Available as experimental build in 3.13.
Never `pickle.load` untrusted input. Prefer `json`, `msgpack`, or protobuf. If you must pickle, sign payloads with HMAC and verify before loading. Consider `restricted` unpicklers that whitelist classes.
Dump stacks with `faulthandler.dump_traceback_later` or `py-spy dump --pid`. Look for threads waiting on locks held by each other. Prevention: consistent lock ordering, use `threading.RLock` where reentrancy is needed, prefer queues over shared state.
Introduced in 3.10 with `match`/`case`. Matches on shape and values, not just equality. Great for ADTs and message dispatch. ```python match ev: case {"type": "click", "x": x, "y": y}: ... case Move(dx, dy): ... ```
`concurrent.futures` provides a unified interface for thread and process pools with blocking APIs. `asyncio` is cooperative single-thread concurrency. You can bridge with `loop.run_in_executor`. Libraries now often provide both blocking and async clients.
Use `python:3.12-slim` or distroless as base. Multi-stage build: install deps in a builder stage, copy only the site-packages into the final image. Use `--no-cache-dir` with pip, remove build tools, pin deps via lockfile, add `.dockerignore`.
Pydantic v2 uses a Rust core (pydantic-core) and is 5-50x faster than v1. API is slightly different (field validators, model_config). Great for API boundaries; overkill for internal data classes. Dataclasses or attrs remain lighter for pure Python logic.
Maintain a dict of topic to list of callbacks. `publish(topic, msg)` iterates callbacks; `subscribe(topic, cb)` appends. For async, store coroutines and `await` them. For robustness across processes, use Redis pub/sub or a message broker.
PEP 420 namespace packages have no `__init__.py` and can be split across multiple directories in `sys.path`. The importer creates a single unified namespace. Useful for plugin ecosystems; avoid mixing with regular packages.
Use `pytest-asyncio`, mark tests with `@pytest.mark.asyncio`. Use `asyncio.get_event_loop` fixtures, `AsyncMock` for async collaborators, and `anyio` if you want a backend-agnostic option.
Run `ruff check` (fast Rust-based linter covering most pycodestyle, pyflakes, and many isort rules) plus `mypy` or `pyright` for types. `black` for formatting. Pre-commit hook ensures violations never land on main.
Use Redis or Postgres as a broker with `NOTIFY`/`LISTEN` or Streams. Workers pop jobs, run them as coroutines inside an `asyncio.Semaphore` for concurrency control, and ack on success. Retries with exponential backoff; dead-letter queue for poison messages. RQ and Arq are good libraries.

Frequently Asked Questions

How long should I prepare for a Python interview?

Most candidates spend 2–4 weeks reviewing fundamentals, data structures, and a few advanced topics like async and decorators. Focus on what the role specifically requires — a data role emphasizes Pandas and SQL; a web role emphasizes Flask/Django/FastAPI.

Do I need to memorize syntax for Python interviews?

You should know common idioms by heart (list comprehensions, dict methods, context managers), but interviewers care much more about problem-solving than exact syntax. If you blank on a method name, ask or pseudocode and move on.

What Python version should I study?

Target Python 3.11 or newer — most companies are on 3.11+ or migrating. Be aware of features from 3.10 (match), 3.11 (exception groups, TaskGroup), and 3.12 (type parameters).

How much data structures and algorithms do I need?

For backend Python roles, expect medium-difficulty LeetCode problems focused on arrays, strings, hashmaps, trees, and graphs. Pure Python knowledge alone will not pass technical loops — pattern practice matters.

What questions are most common in Python interviews?

Mutability, GIL, decorators, generators, list vs tuple, shallow vs deep copy, and how you would structure a real project come up in most screens. Senior rounds add async, performance, and system design.

Related Topics

Ready to apply?

TryApplyNow scores matches, tailors resumes, and tracks applications so you can focus on prep, not paperwork.

Try for free →