415 Python Interview Questions for Data Engineers

415 real Python questions from reported data engineer loops across Meta, Amazon, Google, Netflix, Uber, Airbnb, Citadel, JPMorgan Chase, and hundreds more employers. The 20 below are solved in full: a live editor, a verified solution, and a note on where submissions usually go wrong.

Last updated: Proudly published by: Jeff WahlVerified against 415 live Python problems

Python shows up in about 2 in 3 data engineer loops, as a 45-minute coding round or a take-home with sample data. The bar is different from a software engineering interview: dynamic programming almost never appears, and messy parsing, grouping, intervals, and stateful stream processing always do. These are 20 real questions from reported loops, 1 per company across Meta, Amazon, Google, Netflix, Uber, Airbnb, Citadel, JPMorgan Chase, and 12 more named employers, ordered easy to hard. Every one has a live editor and a verified reference solution with a note on the mistake most submissions make.

Solve them here, or open any question's full problem page for hints and the worked walkthrough. When you finish the 20, the full catalog of 415 Python interview questions continues with the same live validation.

20
questions solved on this page
415
Python questions in the full catalog
20
companies, 1 question each
297
employers tagged across the catalog

What actually comes up in Python interviews

Computed from the 415 Python problems tagged by concept in our catalog, across the 139 employers they were reported from. Percentages are the share of those problems using each concept, so they overlap: a single question usually pulls in three or four at once.

ConceptShare of problemsEmployersWhat comes up
Lists, slicing, and sorting53% (220)97Mutable sequence work, custom sort keys, and list vs tuple immutability tradeoffs.
Dicts and hash-map patterns38% (156)89The single most common Python interview shape: index by key, then aggregate in one pass.
Tuples, unpacking, zip, enumerate34% (141)80Fixed-schema records and paired iteration — immutability as a safety property.
String parsing and formatting31% (129)69Log and CSV line parsing, tokenization, and Unicode-safe handling.
Error handling and guard clauses20% (85)51Failing loudly on bad rows instead of silently dropping them mid-pipeline.
Comprehensions and functional idioms17% (70)47Vectorized-style transforms in pure Python, and when a comprehension hurts readability.
Counting and collections (Counter, defaultdict)16% (68)39Frequency tables without a KeyError, the idiom interviewers expect over manual dict guards.
Sets and membership10% (43)33O(1) membership and set algebra for diffing two ID lists.
Stacks, queues, and interval merging10% (43)34Sessionization and overlapping-interval problems — the DE-flavored algorithm tier.
Recursion and binary search7% (29)23Tree walks over nested JSON and hierarchy tables.
Classes and dunder methods6% (25)17Modeling records as objects, and the dunders that make them sortable and printable.
Generators, yield, and lazy evaluation1% (4)3Streaming a 100GB file on 16GB of RAM by chunking instead of loading it all.

Concept tags come from the same catalog that powers the practice problems, so this table moves as the catalog grows. Counts recomputed hourly.

The Python that comes up in data engineer interviews

8 patterns cover most of what data engineers see in Python rounds. Each maps to questions on this page.

PatternThe canonical formOn this page
Dicts as GROUP BYsetdefault or defaultdict to group, aggregate, then reshapeQuestions 6, 7, 12
Interval merge and sweep linesSort, extend-or-append; or +1/-1 events with a running maxQuestions 11, 19
Two pointers over sorted dataAdvance-only pointers; never restart the inner scanQuestions 3, 18
Single-pass hash mapsOne dict carries state through one loop; no nested scansQuestions 4, 14
Recursion over nested structuresType-check the branch, recurse, mind the string caseQuestions 1, 20
Stateful classes and cachesOrderedDict for LRU, deque for windows, touch semantics decidedQuestions 10, 16
Text parsing and its limitsstdlib and regex first, and name where a real parser takes overQuestions 5, 15
Snapshot diffs and reconciliationIndex by key, set algebra for inserts, deletes, updatesQuestions 9, 17

Easy Python interview questions

Phone-screen warm-ups. The bar is stdlib fluency: the right structure chosen fast, the edge case mentioned unprompted.

Meta logo

1. All the Way Down

Asked in a Data Engineer interview by MetaEasy~10 minFull problem page
Task

A content feed arrives as a list whose entries are grouped into sub-lists nested to arbitrary depth; collapse it into a single flat list of leaf values in their original left-to-right order. A leaf is any non-list value, so a string stays whole rather than being split into its characters, and empty sub-lists contribute nothing.

Show the solution
def flatten(feed):
    result = []
    for item in feed:
        if isinstance(item, list):
            result.extend(flatten(item))
        else:
            result.append(item)
    return result
Netflix logo

2. Quantile Calculator

Asked in a Data Engineer interview by NetflixEasy~10 minFull problem page
Task

Given a list of numbers and percentile (0-100), return the value at that percentile using linear interpolation. The index is percentile / 100 * (n - 1); if fractional, linearly interpolate between the floor and ceiling indices of the sorted values.

Show the solution
def quantile_calculator(data, percentile):
    sorted_data = sorted(data)
    n = len(sorted_data)
    pos = percentile / 100 * (n - 1)
    lower = int(pos)
    upper = lower + 1
    if upper >= n:
        return sorted_data[lower]
    fraction = pos - lower
    result = sorted_data[lower] + fraction * (sorted_data[upper] - sorted_data[lower])
    return result
Goldman Sachs logo

3. The List Merger

Asked in a Data Engineer interview by Goldman SachsEasy~10 minFull problem page
Task

Given two sorted lists a and b, return a single sorted merged list. Do not call sort() or sorted(); use O(n+m) merge.

Show the solution
def merge_sorted(a, b):
    result = []
    i = 0
    j = 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:
            result.append(a[i])
            i += 1
        else:
            result.append(b[j])
            j += 1
    while i < len(a):
        result.append(a[i])
        i += 1
    while j < len(b):
        result.append(b[j])
        j += 1
    return result
Nintendo logo

4. The First Encounter

Asked in a Data Engineer interview by NintendoEasy~15 minFull problem page
Task

Given a string, return a dict mapping each distinct character to the index of its first occurrence.

Show the solution
def first_occurrence(s: str) -> dict:
    result = {}
    for i in range(len(s)):
        ch = s[i]
        if ch not in result:
            result[ch] = i
    return result
Visa logo

5. Twice on the Wire

Asked in a Data Engineer interview by VisaEasy~15 minFull problem page
Task

A card reader retransmits its last status ping whenever an ACK is dropped, so the raw stream arrives with the same status repeated back to back. Collapse each run of consecutive identical pings into a single entry, keeping the surviving pings in their original order.

Show the solution
from itertools import groupby

def collapse_pings(pings):
    return [status for status, _ in groupby(pings)]
Notion logo

6. The Word Census

Asked in a Data Engineer interview by NotionEasy~15 minFull problem page
Task

Given a string of whitespace-separated words, return a dict mapping each distinct lowercased word to its count. (Test harness accepts any key order; note the expected output shows sorted-by-count which may not be guaranteed by dict iteration - be explicit: return a regular dict.)

Show the solution
def word_frequencies(text: str) -> dict:
    words = text.lower().split()
    freq = {}
    for word in words:
        freq[word] = freq.get(word, 0) + 1
    return freq

Intermediate Python interview questions

The core of the round: grouping and reshaping, interval merges, caches with touch semantics, and parsing with its limits named.

JPMorgan Chase logo

7. The Month-by-Month Snapshot

Asked in a Data Engineer interview by JPMorgan ChaseMedium~15 minFull problem page
Task

Given a list of sales records (each a dict with 'employee_id', 'month', 'sales_amount'), return a list of per-employee dicts. For each employee, spread their sales across keys named after each month they sold in, using the month's lowercased three-letter abbreviation (e.g. 'January' -> 'jan', 'February' -> 'feb') as the key and the total sales for that employee-month as the value. Sum the amounts when the same employee has multiple records in the same month. Each output dict also carries the 'employee_id' key. Within each dict, place the month keys first in alphabetical order, then 'employee_id' last. The list of employee dicts is sorted alphabetically by employee_id.

Show the solution
def monthly_sales_crosstab(records: list[dict]) -> list[dict]:
    totals: dict[str, dict[str, int]] = {}
    for record in records:
        emp = record['employee_id']
        key = record['month'][:3].lower()
        amount = record['sales_amount']
        months = totals.setdefault(emp, {})
        months[key] = months.get(key, 0) + amount
    result = []
    for emp in sorted(totals):
        row = {}
        for key in sorted(totals[emp]):
            row[key] = totals[emp][key]
        row['employee_id'] = emp
        result.append(row)
    return result
LinkedIn logo

8. The Top Words

Asked in a Data Engineer interview by LinkedInMedium~15 minFull problem page
Task

Given a text string and integer k, split on whitespace, count occurrences, and return the k words with the highest counts. Sort by count descending, tie-break alphabetically ascending. If fewer than k distinct words exist, return all of them.

Show the solution
def top_k_words(text: str, k: int) -> list:
    words = text.split()
    freq = {}
    for word in words:
        freq[word] = freq.get(word, 0) + 1
    sorted_words = sorted(freq.keys(), key=lambda w: (-freq[w], w))
    result = []
    for i in range(min(k, len(sorted_words))):
        result.append(sorted_words[i])
    return result
Salesforce logo

9. No Days Off

Asked in a Data Engineer interview by SalesforceMedium~15 minFull problem page
Task

You're auditing login records from a retention dashboard, where each entry in activities carries a user_id and a date string in 'YYYY-MM-DD' form, and a user can appear more than once on the same day. Return the user_ids who were active across at least min_streak (default 3) back-to-back calendar days, treating repeated dates for one user as a single day. The result comes back sorted alphabetically.

Show the solution
from datetime import datetime, timedelta

def find_streak_users(activities: list[dict], min_streak: int = 3) -> list[str]:
    user_dates = {}
    for record in activities:
        user = record['user_id']
        date = datetime.strptime(record['date'], '%Y-%m-%d').date()
        user_dates.setdefault(user, set()).add(date)
    streak_users = []
    for user, dates in user_dates.items():
        sorted_dates = sorted(dates)
        max_streak = 1
        current_streak = 1
        for i in range(1, len(sorted_dates)):
            if sorted_dates[i] - sorted_dates[i - 1] == timedelta(days=1):
                current_streak += 1
            else:
                current_streak = 1
            if current_streak > max_streak:
                max_streak = current_streak
        if max_streak >= min_streak:
            streak_users.append(user)
    return sorted(streak_users)
Coinbase logo

10. The Eviction Policy

Asked in a Data Engineer interview by CoinbaseMedium~10 minFull problem page
Task

A read-through cache sits in front of a slow store and holds at most a fixed number of entries; once it is full, the entry that has gone longest without being touched is dropped to make room for a new one. Replay operations against such a cache and return one result per operation, in order. operations[0] is always ['LRUCache', capacity], which sets the entry limit (capacity is at least 1); each later operation is either ['put', key, value], which inserts or overwrites a key, or ['get', key], which returns that key's value or -1 when the key is absent. Both reading and writing a key count as touching it, so the next eviction removes whatever key has stayed idle longest. The constructor and every put contribute None to the result list; each get contributes the value it found, or -1.

Show the solution
from collections import OrderedDict


class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = OrderedDict()

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)
        return self.cache[key]

    def put(self, key: int, value: int):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)
        return None


def the_eviction_policy(operations):
    cache = None
    results = []
    for op in operations:
        name = op[0]
        if name == "LRUCache":
            cache = LRUCache(op[1])
            results.append(None)
        elif name == "put":
            results.append(cache.put(op[1], op[2]))
        elif name == "get":
            results.append(cache.get(op[1]))
    return results
Capital One logo

11. What the Card Remembers

Asked in a Data Engineer interview by Capital OneMedium~20 minFull problem page
Task

A card-authorization service records temporary holds as [start, end] minute offsets, and a single card can accumulate many holds that pile onto each other over a busy day. For each card, collapse the holds that overlap or touch into the continuous windows the money was actually held, earliest first.

Show the solution
def merge_holds(holds):
    by_card = {}
    for hold in holds:
        by_card.setdefault(hold["card"], []).append((hold["start"], hold["end"]))

    result = {}
    for card, windows in by_card.items():
        windows.sort()
        merged = []
        for start, end in windows:
            if merged and start <= merged[-1][1]:
                merged[-1][1] = max(merged[-1][1], end)
            else:
                merged.append([start, end])
        result[card] = merged
    return result
Booking.com logo

12. The Trip Aggregator

Asked in a Data Engineer interview by Booking.comMedium~20 minFull problem page
Task

Given a list of trip dicts (each with user_id, destination, duration_hours), per user_id return user_id, total_hours = SUM(duration_hours), unique_destinations = count of distinct destinations. Return a list of dicts sorted alphabetically by user_id.

Show the solution
def aggregate_trips(trips: list) -> list:
    users = {}
    for trip in trips:
        uid = trip["user_id"]
        if uid not in users:
            users[uid] = {"total_hours": 0, "destinations": set()}
        users[uid]["total_hours"] += trip["duration_hours"]
        users[uid]["destinations"].add(trip["destination"])
    result = []
    for uid in sorted(users.keys()):
        entry = {
            "user_id": uid,
            "total_hours": users[uid]["total_hours"],
            "unique_destinations": len(users[uid]["destinations"])
        }
        result.append(entry)
    return result
Palantir logo

13. Precision and Recall

Asked in a Data Engineer interview by PalantirMedium~10 minFull problem page
Task

Given two equal-length lists of binary labels (actual, predicted), return a dict with 'precision' (TP / (TP + FP)) and 'recall' (TP / (TP + FN)). Use 0.0 when the denominator is 0.

Show the solution
def precision_recall(actual, predicted):
    tp = 0
    fp = 0
    fn = 0
    for i in range(len(actual)):
        if actual[i] == 1 and predicted[i] == 1:
            tp += 1
        elif actual[i] == 0 and predicted[i] == 1:
            fp += 1
        elif actual[i] == 1 and predicted[i] == 0:
            fn += 1
    precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
    recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
    result = {'precision': precision, 'recall': recall}
    return result
Lyft logo

14. The Disputed Fare

Asked in a Data Engineer interview by LyftMedium~15 minFull problem page
Task

A rider disputed a single charge that our billing system actually assembled from two separate ride legs, and finance needs to point to exactly which legs. Given fares, the leg charges in the order they were billed, and a disputed target total, give back the positions of the two legs whose charges add up to it, with the earlier leg first. When more than one such pair exists, return the one that completes earliest as you read left to right, and return None when no two legs account for the total.

Show the solution
def find_fare_pair(fares, target):
    seen = {}
    for j, fare in enumerate(fares):
        complement = target - fare
        if complement in seen:
            return [seen[complement], j]
        if fare not in seen:
            seen[fare] = j
    return None
DoorDash logo

15. The Table Thief

Asked in a Data Engineer interview by DoorDashMedium~12 minFull problem page
Task

You're building a data-lineage tool that needs to know which tables each query reads. Given a SQL query string sql, pull the table names that follow its FROM and JOIN keywords, where a name may be trailed by an alias that isn't part of it.

Show the solution
import re

def extract_table_names(sql):
    pattern = re.compile(r'(?:FROM|JOIN)s+(w+)', re.IGNORECASE)
    names = pattern.findall(sql)
    return sorted(set(names))
Citadel logo

16. The Event Window

Asked in a Data Engineer interview by CitadelMedium~20 minFull problem page
Task

A request-tracking service for an API needs to answer one question on demand: how many hits landed in the last five minutes. Implement a HitCounter whose hit(timestamp) records an event at the given second and whose get_hits(timestamp) returns how many recorded events fall inside the trailing 300-second window ending at and including timestamp, so a hit at exactly timestamp - 300 has already aged out. Timestamps arrive in non-decreasing order. Then implement the graded entrypoint event_window(operations, arguments): given a list of operation names ("HitCounter", "hit", "get_hits") and a parallel list of argument lists, replay them against a single counter and return one result per operation, None for "HitCounter" and "hit" and the integer count for each "get_hits".

Show the solution
from collections import deque


class HitCounter:
    def __init__(self):
        self.hits = deque()

    def hit(self, timestamp: int):
        self.hits.append(timestamp)

    def get_hits(self, timestamp: int) -> int:
        cutoff = timestamp - 299
        while self.hits and self.hits[0] < cutoff:
            self.hits.popleft()
        return len(self.hits)


def event_window(operations, arguments):
    counter = None
    results = []
    for op, args in zip(operations, arguments):
        if op == "HitCounter":
            counter = HitCounter()
            results.append(None)
        elif op == "hit":
            counter.hit(*args)
            results.append(None)
        elif op == "get_hits":
            results.append(counter.get_hits(*args))
    return results

Advanced Python interview questions

The senior tier: snapshot diffs, as-of stream joins, sweep lines, and trees built from edge lists. Structure choice gets questioned line by line.

Amazon logo

17. What the Night Changed

Asked in a Data Engineer interview by AmazonHard~20 minFull problem page
Task

A nightly job dumps a dimension table to a list[dict] and hands you two of them: old_snapshot from yesterday's run and new_snapshot from today's, where each row carries an id_field key that identifies it across runs. Return a dict that splits the differences into inserts (ids seen only today), deletes (ids gone since yesterday), and updates (ids present in both runs whose other fields moved), with every bucket sorted by id_field ascending.

Show the solution
def detect_changes(id_field, new_snapshot, old_snapshot):
    old_index = {}
    for record in old_snapshot:
        old_index[record[id_field]] = record
    new_index = {}
    for record in new_snapshot:
        new_index[record[id_field]] = record
    old_keys = set(old_index.keys())
    new_keys = set(new_index.keys())
    inserts = []
    for k in sorted(new_keys - old_keys):
        inserts.append(new_index[k])
    deletes = []
    for k in sorted(old_keys - new_keys):
        deletes.append(old_index[k])
    updates = []
    for k in sorted(old_keys & new_keys):
        if old_index[k] != new_index[k]:
            updates.append({'new': new_index[k], 'old': old_index[k]})
    return {
        'inserts': inserts,
        'deletes': deletes,
        'updates': updates,
    }
Google logo

18. The Stream Joiner

Asked in a Data Engineer interview by GoogleHard~25 minFull problem page
Task

You're correlating two time-sorted event streams where each event is a dict carrying a timestamp and a value, given a numeric tolerance. Walking stream_a in order, pair each event with the single nearest event in stream_b whose timestamp lies within tolerance of it, emitting a dict of a_value, b_value, and the absolute gap; an A event with nothing inside its window contributes no row. When two B events are equally near, keep the earlier one, and remember a single B event can be the nearest match for several A events.

Show the solution
def windowed_join(stream_a: list, stream_b: list, tolerance: float) -> list:
    matches = []
    left = 0
    for a_event in stream_a:
        a_time = a_event["timestamp"]
        # Advance the shared pointer past B events too early for this (and every later) A.
        while left < len(stream_b) and stream_b[left]["timestamp"] < a_time - tolerance:
            left += 1
        best_b = None
        best_gap = float("inf")
        j = left
        while j < len(stream_b):
            b_time = stream_b[j]["timestamp"]
            if b_time > a_time + tolerance:
                break
            gap = abs(a_time - b_time)
            if gap < best_gap:
                best_gap = gap
                best_b = stream_b[j]
            j += 1
        if best_b is not None:
            matches.append({
                "a_value": a_event["value"],
                "b_value": best_b["value"],
                "gap": float(best_gap),
            })
    return matches
Uber logo

19. No Vacancy

Asked in a Data Engineer interview by UberHard~20 minFull problem page
Task

A room-booking service holds a day's calendar as meetings, a list of [start, end] intervals, and needs the fewest rooms that can host all of them without two live meetings sharing a room. Treat the intervals as half-open: a meeting ending at t frees its room for one starting at t, and an empty schedule needs zero rooms.

Show the solution
def min_rooms(meetings):
    events = []
    for start, end in meetings:
        events.append((start, 1))
        events.append((end, -1))
    events.sort()
    current_rooms = 0
    max_rooms = 0
    for time, delta in events:
        current_rooms += delta
        if current_rooms > max_rooms:
            max_rooms = current_rooms
    return max_rooms
Airbnb logo

20. Where the Buck Stops

Asked in a Data Engineer interview by AirbnbHard~25 minFull problem page
Task

A directory export gives you reporting lines as (manager, report) pairs in no particular order, and you need the org chart back. Return the reporting hierarchy as nested dictionaries, with the one person nobody reports to at the top.

Show the solution
def build_tree(pairs: list[tuple[str, str]]) -> dict:
    children: dict[str, list[str]] = {}
    reports = set()
    for manager, report in pairs:
        children.setdefault(manager, []).append(report)
        children.setdefault(report, [])
        reports.add(report)

    roots = [person for person in children if person not in reports]
    if not roots:
        return {}

    def subtree(person: str) -> dict:
        return {report: subtree(report) for report in children[person]}

    return {roots[0]: subtree(roots[0])}

Rapid-fire Python concept questions

The verbal questions between coding prompts. Data engineer rounds lean on the stdlib and the data model, not trivia.

What goes wrong with a mutable default argument?

def f(x, acc=[]) evaluates the default once at definition time, so every call without acc shares one list and results accumulate across calls. The fix is acc=None with an inside-the-function default. Interviewers plant this in code-review prompts because it looks correct and fails statefully.

Is dict ordered in Python?

Insertion-ordered since 3.7, guaranteed by the language. The distinction that matters: iteration order is insertion order, not sorted order and not frequency order, so a counting dict does not come back most-common-first. OrderedDict still earns its keep for move_to_end and popitem(last=False), which is why LRU implementations reach for it.

When do you use a generator instead of a list?

When the data does not need to exist all at once: streaming a large file, chaining transforms, or short-circuiting on the first match. A generator holds one element of state; a list holds everything. In pipeline code the difference is whether a 10 GB input fits in memory, which makes this a favorite data engineering follow-up.

Why are sets the answer to most membership questions?

x in list scans; x in set hashes. Inside a loop that turns O(n) into O(n squared) versus O(n). The reflex to demonstrate: any repeated membership check against a collection you built yourself belongs in a set, and the conversion cost is paid once.

What does sorted()'s key parameter buy you, and is the sort stable?

key maps each element to its sort value once, and Python's sort is stable, so equal keys keep their original order. Composite contracts like count descending then word ascending compress into one key tuple, (-count, word). Stability also means you can sort twice for layered orderings, minor key first.

Shallow copy versus deep copy?

A shallow copy duplicates the outer container and shares every nested object; deepcopy recurses. The practical bite: copying a list of dicts shallowly and mutating a row mutates the original. In pipeline code the safer habit is building new structures instead of copying and mutating at all.

What are Counter and defaultdict for?

Counter is a dict subclass for tallies with most_common built in; defaultdict removes the missing-key branch from grouping code. Both signal stdlib fluency in an interview, and both have a one-line handwritten equivalent (get with a default, setdefault) worth knowing when the interviewer asks you to avoid imports.

is versus ==?

== compares values; is compares identity. The only routine correct use of is in application code is None checks (x is None). Small-integer and string interning make is appear to work on values in quick experiments and then fail in production, which is exactly why the question gets asked.

Does the GIL matter for data engineering Python?

Less than the question implies. The GIL serializes CPU-bound threads, but pipeline code is usually I/O-bound (threads fine) or hands the heavy lifting to C libraries and engines (pandas, polars, Spark) that release it. The senior answer names the boundary: multiprocessing or a distributed engine for CPU-bound transforms, threads or async for I/O fan-out.

When do you reach for pandas in an interview, and when not?

If the prompt is genuinely tabular (group, join, pivot on rows and columns), pandas is fine and fast to write. If the prompt is about structures, streams, or state, stdlib answers read stronger and avoid version trivia. Ask the interviewer whether libraries are in bounds; the question itself is a good scoping signal.

How do you process a 100GB CSV on a machine with 16GB of RAM?

Stream it rather than load it. In pandas that is read_csv with a chunksize, processing each chunk and accumulating only the aggregate you need. In pure Python it is iterating the file object line by line, which is already lazy. If the transform needs shuffles or joins across the whole set, that is the signal to move to a distributed engine like Spark rather than to a bigger machine.

What are generators and the yield keyword, and why do they matter for big data?

A generator function returns an iterator that produces values one at a time and suspends its state between them, so it holds one element in memory instead of the whole sequence. That is lazy evaluation: a chain of generators streams a dataset through several transforms without ever materializing an intermediate list. The tradeoff is that a generator is single-pass and has no length.

What is vectorization in NumPy and why prefer it to a Python loop?

Vectorized operations apply to a whole array at once inside compiled C, so the per-element interpreter overhead and the boxing of Python objects disappear. A loop over a million floats pays a million interpreter steps; the vectorized form pays one dispatch. It also uses contiguous typed memory, which is cache-friendly. The rule in review is that an explicit element-wise loop over a NumPy array is almost always the wrong shape.

What are Python decorators and how would a data engineer use one?

A decorator is a callable that wraps a function and returns a replacement, applied with @-syntax. In pipeline code they carry cross-cutting concerns without touching the body: timing and logging a task, retrying with backoff on a transient failure, caching with functools.lru_cache, or registering a function into a task registry. Use functools.wraps so the wrapped function keeps its name and docstring.

What is a context manager and when do you write one?

An object implementing __enter__ and __exit__, used with the with statement so setup and teardown are guaranteed even when the body raises. You write one when a resource must be released deterministically: a database connection, a file handle, a temp directory, or a transaction that must roll back on error. contextlib.contextmanager turns a generator into one with far less ceremony.

What are dataclasses and why use one over a dict?

A dataclass generates __init__, __repr__, and __eq__ from typed field declarations, giving you a record with named, type-annotated fields. Over a dict it buys attribute access, static checking, and a schema that is visible in the code rather than implied by whatever keys happen to be present. frozen=True makes instances hashable and immutable, which is what you want for a pipeline config.

How does Python manage memory and garbage collection?

CPython uses reference counting as the primary mechanism: an object is freed the moment its count hits zero. Because reference cycles never reach zero, a generational cyclic collector runs periodically to find and break them. The practical consequences are that holding one reference to a big object keeps it all alive, and that __del__ ordering is not something to depend on.

What is the difference between multiprocessing and multithreading here?

Threads share one interpreter and one GIL, so they overlap I/O waits but not CPU work. Processes each get their own interpreter and memory, so they use multiple cores, at the cost of pickling data across the boundary. For a data ingestion task dominated by network waits, threads or asyncio win. For CPU-bound transforms, multiprocessing or an engine that drops into C.

What is pickling and what are its risks?

Pickle serializes a Python object graph to bytes. The risks: unpickling executes arbitrary code, so it must never be pointed at untrusted input; the format is Python-specific and version-fragile, so it is a poor choice for durable or cross-language storage; and it silently drops things like open file handles. Parquet, JSON, or Avro are the right answers for data at rest.

What is the difference between a list and a tuple in pipeline code?

A list is mutable and sized for growth; a tuple is immutable and fixed. In data engineering the immutability is the point: a tuple is hashable, so it can key a dict or join a set, and it signals a fixed-schema record whose shape will not shift under you. Lists are for accumulating; tuples are for records and composite keys.

Getting interview-ready on Python

Four capabilities that decide Python screens, roughly in the order worth building them. For engineers who write Python at work but have not interviewed in it lately.

  1. 01

    Make core structures automatic

    Dicts, sets, list and dict comprehensions, string methods, enumerate and zip. These should take 5 to 8 minutes without deliberation, so a warm-up question costs no clock and the round is spent on what actually decides it.

    • Reach for enumerate instead of a manual index until it is reflex.
    • Say the complexity of each membership check out loud as you write it.
  2. 02

    Own grouping, intervals, and dates

    The dict-as-GROUP-BY shape, interval merge, streak detection with date arithmetic, top-K with composite sort keys. Most Python screens turn on this tier, and it is the SQL round's patterns restated in another language.

    • Derive interval merge from first principles: sort, then extend or append.
    • Normalize before you aggregate: parse dates, lowercase keys, dedupe per group.
  3. 03

    Put state in the right structure

    LRU and windowed counters, two-pointer walks over sorted streams, generators for large inputs, snapshot diffs. Senior loops probe whether your state lives where it belongs: deque for windows, OrderedDict for recency, sets for seen-ness.

    • Build an LRU from scratch twice; the touch-on-get is what slips.
    • For any two-collection problem, ask whether sorting one enables a single pass.
  4. 04

    Handle take-homes and work out loud

    Take-homes are won on messy-data handling and readable structure, not cleverness: parse defensively, name the edge cases in comments, include a couple of checks that prove correctness. Then run timed mock rounds with narration, because the live round weighs the conversation as much as the code.

    • Record one session. The silent stretches are what an interviewer remembers.
    • Close every run by naming the follow-up you would expect, then answering it.

The mistakes that fail Python screens

From submissions across the catalog, these are the recurring failure modes, not syntax errors.

Iterating a string like a list

Strings are iterable, so a flatten or a recursive walk that type-checks with iterability explodes every string into characters. Check isinstance(x, list) or (list, tuple) explicitly. Question 1 plants exactly this.

Quadratic membership checks

value in some_list inside a loop reads innocently and scans every time. Build a set first. On the hidden cases with large inputs this is the difference between passing and timing out.

Mutating while iterating

Deleting dict keys or list elements inside the loop that iterates them raises or silently skips. Collect first, mutate after, or build a new structure. Interviewers read the new-structure habit as production instinct.

Trusting dict order to mean something

Insertion order is guaranteed; sorted order and frequency order are not. Any output contract with an ordering needs an explicit sorted() with the right key, usually a composite one.

Returning the working structure instead of the contract

The set of destinations instead of its count, the datetime instead of the string, None instead of an empty list. Re-read the output contract before returning; deterministic ordering is part of it.

Skipping the touch semantics on stateful questions

In an LRU, a read refreshes recency and an overwrite refreshes recency; in a windowed counter, eviction happens on read. State machines are where hidden cases concentrate, because the happy path passes without them.

How the Python round runs

A 45-minute Python round opens with a warm-up on structures, then one multi-part problem that grows: parse this, now group it, now handle the malformed rows, now make it fast. The interviewer is watching structure choice and narration: whether the state lands in a deque or a dict, whether you name the complexity as you go, whether the edge cases arrive before the follow-up asks for them.

Take-homes flip the rubric. 4 hours with sample data is won on defensive parsing, readable decomposition, and a handful of checks that prove the output, not on cleverness. Reviewers consistently reward code that states its assumptions and handles the malformed line over code that is shorter.

Libraries are usually in bounds but rarely required. The strongest signal is stdlib fluency: itertools.groupby where it fits, Counter for tallies, heapq when K is small and N is not, and the judgment to say when a real parser or a dataframe library is the production answer even though the interview answer is 20 lines of stdlib.

Prepare for the interview
01 / Open invite
02min.

Know the patterns before the interviewer asks them.

a Python query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1def sessionize(events):
2 sessions = []
3 for e in events:
4 if gap_minutes(e) > 30:
5
Execute your solution0.4s avg.
ShopifyInterview question
Solve a problem

415 Python questions with hidden test cases

These 20 are the most-reported shapes from a much larger pool. The full Python practice catalog has 415 questions from reported data engineer interviews, filterable by pattern, difficulty, and company. Every submission runs live and is checked against hidden cases that probe the edges the sample input hides: empty collections, ties, malformed rows, and inputs large enough to expose a quadratic scan.

When problems feel solved, interview mode asks the same questions the way an interviewer does: a vague prompt, a timer, follow-ups, and a verdict on the conversation as much as the code.

Shortest Unique Metric Tag

> A metrics platform stores fully-qualified metric names like 'bookings.checkout.success_rate'. Dashboards autocomplete a metric from any contiguous substring of its name, so each metric needs the shortest substring of its own name that does not appear inside any other metric's name. Given a list of metric names, return a list of the same length where the i-th entry is the shortest contiguous substring of metrics[i] that is not a substring of any other metric in the list. If multiple substrings of the same metric tie for the shortest length, return the lexicographically smallest one. If every substring of metrics[i] also appears in some other metric's name, return an empty string for that metric.

Sample input & expected output(3 examples)
Input · example 1
metrics:["clicks","clocks","cluster"]
Output
["i","o","e"]
Input · example 2
metrics:["bookings","booking_id","books"]
Output
["gs","_","ks"]
Input · example 3
metrics:["pipeline_run","pipeline_runs"]
Output
["","s"]

Python data engineer interview questions: FAQ

Are these Python interview questions from real interviews?+
Yes. Questions come from interview reports submitted by data engineer candidates, deduplicated and rewritten so the shapes match what surfaced without copying prompt text. The company on each question means at least one report cited that employer for that question shape.
How much Python do data engineer interviews expect?+
Fluency with the stdlib and the data model: dicts, sets, comprehensions, sorting with keys, generators, and enough class design to build an LRU or a windowed counter. Deep framework knowledge is rarely probed; deep comfort with messy data always is.
Do I need LeetCode-style dynamic programming for data engineer roles?+
Almost never. The overlap with software engineering interviews is arrays, hashing, two pointers, and intervals. The divergence is everything data-shaped: grouping, reshaping, snapshot diffs, streams, and date arithmetic. Prep time spent on DP is prep time taken from the patterns above.
Is pandas required in Python interview rounds?+
Usually allowed, rarely required, occasionally banned to check fundamentals. If the prompt is truly tabular, pandas is a fine answer; if it is about structures or state, stdlib reads stronger. Take-homes are the exception: reviewers expect whichever tool a production engineer would pick, used cleanly.
How many Python questions is enough before a data engineer screen?+
30 to 50 across easy and medium, spread over the 8 patterns on this page. Pattern recognition beats volume: once dict-as-GROUP-BY, interval merge, and single-pass hash maps are automatic, most prompts turn out to be one of them with different nouns.
What is different about a data engineering take-home?+
Sample data arrives dirty on purpose: mixed types, duplicate keys, malformed rows, timezone surprises. The rubric rewards defensive parsing, stated assumptions, decomposed functions, and a few checks proving the output. Cleverness is worth less than an honest comment naming a tradeoff.
Can I run these questions against real checks?+
Yes. The editor under each question runs your code live and submits against hidden cases, and each question's full problem page adds hints and the worked walkthrough. The hidden cases probe edges the sample hides, which is where interview code usually breaks.
How do Python rounds differ at Meta, Amazon, and the trading firms?+
The patterns repeat; the emphasis moves. Meta reports lean on parsing and nested structures, Amazon on reconciliation and snapshot shapes, and trading firms like Citadel on stateful stream processing under tighter complexity scrutiny. Every question on this page names the employer it was reported from.
Python or SQL: which decides the data engineer loop?+
SQL appears in more loops (95% versus about 2 in 3), but the Python round is where mid-level candidates separate, because it exposes structure choice and state handling that SQL hides. Treat SQL as the ticket in and Python as the tiebreaker.
What is the difference between practicing problems and practicing the interview?+
Problems give you a clear prompt and instant feedback; the interview takes both away and adds follow-ups. Explaining code out loud is a separate skill from writing it, and most candidates first practice it in the interview itself. Mock interview mode exists so that first time is not the real one.
02 / Why practice

Solve the next one under interview conditions

  1. 01

    Reading a solution is not the same as writing one

    Every engineer who has frozen on a query they had read a dozen times knows the gap. The only preparation that closes it is producing the answer yourself, under time, before the interview does it for you

  2. 02

    76% of hiring managers reject on the coding task, not the resume

    From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice

  3. 03

    5 problem shapes cover 80% of data engineer loops

    Parsing and reshaping, sessionization, dedup with tie-breaks, streaming aggregation, top-N-per-group. Writing them by hand turns the unfamiliar into pattern recognition

Keep going