Which Python basics do interviewers check?
For data roles, Python rounds often test readable problem solving, data structures, file handling, and whether you can write code that handles edge cases.
Practice Python interview questions covering basics, data structures, functions, exceptions, file handling, comprehensions, OOP basics, pandas, and data-role coding.
By Durgesh Yadav — Senior Data Engineer @ 7-Eleven · Updated 2026-07-29. Preparation guidance, not a hiring guarantee.
Data-role Python interviews focus on practical work, not trick puzzles: list and dictionary manipulation, string parsing, pandas DataFrame operations (filtering, groupby, merges), file and API handling, and writing small, clean functions. Product companies may add complexity questions; services companies stay closer to syntax and data-cleaning scenarios.
Guide
For data roles, Python rounds often test readable problem solving, data structures, file handling, and whether you can write code that handles edge cases.
Practice explaining your approach before coding. A clear brute-force answer with edge cases is better than a clever answer you cannot debug.
Frequency hint
Normalize text, split into tokens, use a dictionary or Counter, and decide how to handle punctuation.
Data cleaning hint
State how you handle missing values, type conversion, invalid dates, and duplicate rows.
For analytics and data engineering, Python questions may involve parsing files, calling APIs, transforming rows, or explaining pandas-style operations.
Many candidates lose points by ignoring edge cases, mutating inputs unexpectedly, writing unreadable one-liners, or not explaining complexity.
Answered questions that come up repeatedly in data and automation rounds. Aim for a clear, correct explanation over a clever one-liner.
List vs tuple
Lists are mutable; tuples are immutable and hashable, so a tuple can be a dict key or set member and signals a fixed record. Use a tuple when the collection should not change.
Shallow vs deep copy
copy.copy() copies the outer object but shares nested objects, while copy.deepcopy() recursively copies everything. After a shallow copy, mutating a nested list changes both copies.
is vs ==
== compares values; is compares identity (the same object in memory). Use == for equality and reserve is for checks like 'if x is None'.
Mutable default argument trap
def f(x, acc=[]) reuses the same list across calls because the default is created once. Use acc=None, then set acc = acc or [] inside the function.
Count word frequency from a file
Read the file, lower-case and split into tokens, then use collections.Counter(tokens). Counter.most_common(n) returns the top n words; decide how to strip punctuation first.
Generators vs lists
A generator yields items lazily with near-constant memory, ideal for large or streaming files; a list holds everything at once. Use yield or a generator expression for big data.
Remove duplicates preserving order
Use dict.fromkeys(items) which keeps first-seen order, or loop with a seen set. A plain set() removes duplicates but loses order.
groupby and merge in pandas
df.groupby('key')['val'].agg(...) splits rows by key and aggregates each group; pd.merge(a, b, on='key', how='left') joins like SQL. Watch many-to-many keys, which inflate row counts.
Question bank
Real questions from beginner to advanced, each with a concise model answer — practice them, then rehearse live in a mock interview.
A list is mutable — you can append, remove and reassign elements — while a tuple is immutable: once created, its contents cannot change. Lists use square brackets ([1, 2]), tuples use parentheses ((1, 2)). Because tuples are immutable they are hashable (usable as dict keys and set members) when their elements are hashable, slightly faster to iterate, and safer as 'records' whose shape should never change — like (lat, lon) coordinates. Interviewers often follow up with: a tuple containing a list, e.g. t = ([1], 2), is still a tuple, but t[0].append(3) works — immutability applies to the tuple's references, not to the objects they point at.
A list is ordered, allows duplicates, and supports indexing/slicing. A set is unordered, stores only unique hashable elements, and supports O(1) average membership tests plus set algebra (union |, intersection &, difference -). Use a set when the question is 'have I seen this before?' — converting a lookup loop from list to set turns an O(n²) de-duplication into O(n). The classic interview demonstration: len(set(items)) < len(items) detects duplicates in one line. Caveat: sets cannot hold unhashable values like lists or dicts, and they do not preserve insertion order (dict does, since Python 3.7).
Mutable objects can change in place after creation: list, dict, set, bytearray, most user-defined classes. Immutable objects cannot: int, float, str, tuple, frozenset, bytes. This matters in three places interviewers probe. (1) Function arguments: passing a mutable object lets the callee modify the caller's data. (2) Default arguments: def f(x=[]) shares ONE list across all calls. (3) 'Modifying' a string like s += 'x' actually allocates a new string each time — which is why ''.join(parts) beats += in a loop. Identity check: after s = 'ab'; s += 'c', id(s) has changed; after lst.append(3), id(lst) has not.
'==' compares values by calling __eq__; 'is' compares identity — whether both names point at the same object in memory. [1,2] == [1,2] is True, but [1,2] is [1,2] is False because they are two distinct objects. The only idiomatic uses of 'is' are for singletons: x is None, x is True, x is NotImplemented. The interview trap: small integers (-5..256) and short strings are interned by CPython, so 256 is 256 happens to be True while 257 is 257 may be False — code relying on that is broken by design, which is exactly why comparisons should use '=='.
A list comprehension builds a list from an iterable in a single expression: squares = [x*x for x in range(10) if x % 2 == 0]. It replaces a for-loop with append, is usually faster (the loop runs in C), and reads declaratively. Variants: dict comprehensions {k: v for ...}, set comprehensions {x for ...}, and generator expressions (x for ...) which produce items lazily instead of materialising a list. Interview follow-up: nested comprehensions like [cell for row in grid for cell in row] flatten a matrix — the for-clauses read left-to-right in the same order as nested loops. Prefer a plain loop when the logic needs multiple statements; an unreadable comprehension is worse than a loop.
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. def f(*args, **kwargs) accepts any call signature, which is how decorators and wrappers forward arguments unchanged: return func(*args, **kwargs). The same stars unpack in the other direction at call sites: f(*[1,2], **{'x': 3}) is f(1, 2, x=3). Two details worth volunteering: parameters after * are keyword-only (def f(a, *, b) forces f(1, b=2)), and Python 3.8's / marks positional-only parameters. Order in a signature is: positional, *args, keyword-only, **kwargs.
CPython uses reference counting as its primary mechanism — every object tracks how many references point to it, and when the count hits zero the memory is freed immediately. A cyclic garbage collector supplements this, because reference counting alone cannot free reference cycles (a.b = b; b.a = a). Small objects are allocated from pooled arenas by pymalloc rather than the OS directly. Practical implications interviewers look for: del removes a name, not necessarily the object; sys.getrefcount(x) shows the count (plus one for the call itself); circular references with __del__ used to be uncollectable; and long-running processes can hold memory because arenas are not always returned to the OS — which is why large batch jobs often fork workers instead.
A list materialises every element in memory immediately; a generator produces elements one at a time, on demand, remembering its position between next() calls. gen = (x*x for x in range(10**9)) is instant and uses constant memory, while the list equivalent would need gigabytes. Generators are written as functions with yield or as generator expressions. The trade-offs: generators are single-pass (once consumed, they are exhausted), have no len(), and cannot be indexed. They shine in pipelines — sum(x*x for x in nums if x > 0) — and for streaming large files. A follow-up worth pre-empting: yield from delegates to a sub-generator, and generators underpin how 'for' loops, itertools and async code work.
A decorator is a callable that takes a function and returns a replacement, applied with @name above the definition. It is the standard way to add behaviour — timing, caching, auth checks, retries — without editing the wrapped function. Minimal pattern: def log(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(func.__name__); return func(*args, **kwargs) return wrapper. The @functools.wraps line is the detail that separates candidates: without it the wrapper hides the original's name and docstring. Decorators with their own arguments need one more nesting level (a factory returning a decorator). Built-ins used daily: @property, @staticmethod, @functools.lru_cache.
The GIL is a mutex in CPython that lets only one thread execute Python bytecode at a time. It simplifies memory management (reference counts are not thread-safe to update concurrently) at the cost of parallel CPU-bound threading: two compute-heavy threads on two cores still run one-at-a-time. What still works under the GIL: I/O-bound threading (the GIL is released while waiting on sockets/disk), C-extension code that releases it (NumPy does), multiprocessing (separate interpreters, true parallelism), and asyncio for high-concurrency I/O. The senior-level nuance: the GIL is a CPython implementation detail, not a language rule — and PEP 703's optional free-threaded build is changing this story.
A shallow copy (copy.copy, list(x), dict(x), x[:]) creates a new outer container whose elements are the SAME inner objects; a deep copy (copy.deepcopy) recursively copies everything. The classic failure: b = copy.copy(a) where a = [[1,2],[3,4]], then b[0].append(9) — a[0] also shows 9, because both lists share the inner lists. Deep copy avoids that but costs time, memory, and needs care with cycles (deepcopy handles them via a memo dict). In data code the same idea appears as df.copy() vs df.copy(deep=False) in pandas, and views vs copies in NumPy — a slice of an ndarray is a VIEW, mutating it mutates the original.
try/except catches exceptions; else runs only if no exception occurred; finally always runs (cleanup); raise re-raises or throws. Catch the NARROWEST exception you can — except Exception: pass is the classic anti-pattern because it silently eats bugs like AttributeError and KeyboardInterrupt subclasses of your intent. raise NewError(...) from err chains exceptions so the original traceback survives. Custom exceptions are plain classes: class PaymentDeclined(Exception): pass — used to give callers something precise to catch. EAFP ('easier to ask forgiveness than permission') is idiomatic Python: try/except KeyError often beats checking 'if key in dict' twice under concurrency.
A context manager guarantees setup/teardown around a block: with open(path) as f: ... closes the file even if the body raises. The protocol is two methods — __enter__ (returns the resource) and __exit__ (cleans up; returning True suppresses the exception). The lighter way to write one is @contextlib.contextmanager with a single yield: everything before it is setup, everything after runs on exit. Real uses beyond files: acquiring locks, database transactions (commit on success, rollback on exception), temporarily changing a working directory, and pytest's 'with pytest.raises(ValueError):' assertion.
def add(item, bucket=[]) evaluates the default ONCE, at function definition — so every call without a bucket shares the same list, and items mysteriously accumulate across calls. The fix is a sentinel: def add(item, bucket=None): if bucket is None: bucket = []. The same trap applies to dict/set defaults and to any mutable object constructed in the signature, including default arguments like now=datetime.now() — that timestamp is frozen at import time, not call time. This question is a favourite precisely because the broken version works in simple tests and fails only in production-shaped call patterns.
Core: list (ordered, mutable), tuple (immutable record), dict (O(1) key lookup, insertion-ordered since 3.7), set (uniqueness and membership). Standard library upgrades interviewers like to hear: collections.defaultdict (grouping without key checks), Counter (frequency counting in one line), deque (O(1) appends/pops at both ends — the right BFS queue), namedtuple/dataclasses (readable records), and heapq (priority queues, top-k problems). For actual data engineering scale: NumPy arrays (contiguous, vectorised), pandas DataFrames (labelled columns), and pyarrow Tables (zero-copy columnar interchange). Choosing the right one IS the interview answer: 'count word frequencies' should be Counter(words).most_common(10), not a hand-rolled dict loop.
map(f, xs) applies a function lazily; filter(pred, xs) keeps matching items lazily; both return iterators in Python 3. A list comprehension does either or both eagerly and is generally preferred as more readable: [f(x) for x in xs if pred(x)] replaces list(map(f, filter(pred, xs))). map/filter win when you already have a named function (map(str.strip, lines) is clean) or when you want laziness without a generator expression. The functional trio's third member, reduce, moved to functools and is usually better expressed with sum(), max(), or an explicit loop. Mentioning that comprehensions avoid lambda-call overhead — and are therefore usually faster than map with a lambda — lands well.
Python 3.9+: merged = a | b (b's values win on key conflicts); in-place: a |= b. Python 3.5+: {**a, **b}. Older / still common in codebases: merged = a.copy(); merged.update(b). For nested structures none of these recurse — a shallow merge replaces entire sub-dicts, so deep merges need a small recursive helper or a library. Related tools worth naming: dict.setdefault for insert-if-missing, collections.ChainMap for a LAYERED view that does not copy at all (first match wins — handy for config-over-defaults), and the fact that later sources always override earlier ones in every merge form.
An iterable is anything you can loop over — it implements __iter__ returning an iterator (lists, strings, dicts, files, generators). An iterator is the stateful cursor doing the walking — it implements __next__ (produce the next item or raise StopIteration) and __iter__ (return itself). A for-loop calls iter() on the iterable, then next() repeatedly, catching StopIteration. Key consequences: iterables like lists can be looped many times; iterators are one-shot — zip, map, filter, and generators all exhaust. The subtle bug this explains: assigning it = iter(xs), consuming part of it in one loop, then wondering why a second loop starts midway. Files are their own iterators, which is why you can only read them through once.
Stream it, never call .read() or .readlines(). For text, the file object is already a lazy line iterator: with open(path) as f: for line in f: process(line) — constant memory regardless of file size. For binary, read fixed chunks: while chunk := f.read(1024*1024). For CSVs, csv.reader streams rows; in pandas, pd.read_csv(path, chunksize=100_000) yields DataFrame chunks you aggregate incrementally, and specifying dtype/usecols cuts memory further. For columnar analytics at scale, mention Parquet + predicate pushdown via pyarrow/DuckDB, which reads only the needed columns and row groups. If the follow-up is 'and sort it?' — external merge sort: sort chunks, write runs, heapq.merge the runs.
A closure is an inner function that captures variables from its enclosing scope and keeps them alive after the outer function returns: def make_counter(): n = 0; def inc(): nonlocal n; n += 1; return n; return inc. Each make_counter() call gets independent state — lightweight encapsulation without a class. Closures are the mechanism behind decorators and factory functions like def multiplier(k): return lambda x: x * k. Two interview details: 'nonlocal' is required to REBIND a captured variable (reading needs nothing), and the late-binding trap — functions created in a loop all see the loop variable's FINAL value unless you pin it with a default: lambda x, k=k: x * k.
In priority order: (1) Eliminate row loops — replace iterrows/apply with vectorised column operations; df['c'] = df.a * df.b runs in C. (2) Fix dtypes — object columns are Python-object arrays; convert repeated strings to 'category', numbers out of object with pd.to_numeric, and use nullable dtypes. (3) Load less — usecols/dtype/parse_dates in read_csv, or switch to Parquet. (4) Prefer merge/groupby/transform over manual loops, and query()/boolean masks over Python-level filtering. (5) Watch copies — chained indexing (df[a][b] = x) both warns and silently misses; use .loc. (6) When one core is the limit, mention the escape hatches by name: numba for custom numerics, DuckDB/Polars for large joins and aggregations — knowing WHEN pandas is the wrong tool reads as seniority.
Threads share one interpreter and memory space, but the GIL serialises Python bytecode — so threading helps I/O-bound work (network calls, disk) and does NOT speed up CPU-bound pure-Python code. Multiprocessing launches separate interpreter processes: true parallelism for CPU-bound work, at the cost of process startup and pickling data across process boundaries. Practical guidance interviewers want: use concurrent.futures.ThreadPoolExecutor for many API calls, ProcessPoolExecutor for CPU-heavy transforms, asyncio when you need thousands of concurrent I/O tasks in one thread, and remember NumPy/pandas often release the GIL inside C code, so threads CAN help there. Rule of thumb: I/O-bound → threads or asyncio; CPU-bound → processes.
By company
See how this topic shows up in real Data Analyst loops — rounds, difficulty and company-specific questions:
Related Guides
Move between roadmaps, interview questions, and tools without losing your preparation thread.
Move from Python foundations into distributed data processing.
Read guideSee where Python fits in data engineering.
Read guidePair Python with SQL for data-role interviews.
Read guideReturn to the parent resource hub for the full preparation path.
Open hubFAQ
Most entry and mid-level data roles need strong basics, data structures, file handling, API/CSV work, and practical pandas-style transformation.
Memorize fundamentals, but prioritize explaining approach, edge cases, and why a data structure is appropriate.
Not always. They are common in data analyst and data science workflows, but many interviews still test core Python first.
Build scripts that read data, validate it, transform it, and write clean output. Then explain failure handling.
Avoid unclear one-liners, missing edge cases, broad exception swallowing, and claims about libraries you have not used.
Use Open Learning for revision, AI Mock Interview for practice, and Rewrite My Resume to turn Python projects into honest resume proof.
Next Step
Use PrepNPlaced tools to turn this learning path into resume proof, targeted practice, and interview-ready explanations.