Python Interview Questions for Data Roles

The plain-Python coding tasks data-analyst and data-engineer interviews use, without pandas: counting, deduplicating, grouping, streaming large files, flattening JSON, joining, and the bugs interviewers plant. Each has a worked solution and the follow-up questions to expect.

GloryTecks Team

Training & Placement Faculty

3 June 2026 8 min read

Last updated: 24 September 2026

Share:

A live coding round for a data role may ask for plain Python, without pandas, to see whether you understand the operations underneath the libraries. The tasks look simple. What they test is whether you choose the right data structure, handle messy input, and know what happens when the data is 100 times bigger. This article collects ten such tasks, each with a solution that has been run, its cost, and the follow-up questions that naturally come next.

What this set is not

For conceptual Python questions (generators, decorators, the GIL and so on), use Python interview questions: top 50 with answers. For DataFrame questions, see pandas interview questions. Array questions are in NumPy interview questions for data roles.

Counting and deduplicating

1. Return the k most frequent values

python
from collections import Counter

def top_k(values, k):
    return Counter(values).most_common(k)

top_k(["IN", "US", "IN", "UK", "US", "IN", "DE"], 2)   # [('IN', 3), ('US', 2)]

What it tests: knowing Counter instead of hand-writing a dictionary loop. Follow-ups: "How are ties ordered?" Elements with equal counts come out in the order they were first encountered (Python docs: collections), so say so, or sort explicitly if the interviewer wants alphabetical ties. "What if the values come from a 50 GB log?" Read it line by line and call counter.update() per line; memory then grows with the number of distinct values, not the file size.

2. Deduplicate records, keeping the latest per key

python
from datetime import datetime

def latest_per_key(records, key="customer_id", ts="updated_at"):
    latest = {}
    for r in records:
        k = r[key]
        if k not in latest or datetime.fromisoformat(r[ts]) > datetime.fromisoformat(latest[k][ts]):
            latest[k] = r
    return list(latest.values())

rows = [
    {"customer_id": 7, "email": "old@x.com", "updated_at": "2026-03-01T09:00:00+05:30"},
    {"customer_id": 9, "email": "a@y.com",   "updated_at": "2026-03-02T10:00:00+05:30"},
    {"customer_id": 7, "email": "new@x.com", "updated_at": "2026-03-04T08:30:00+05:30"},
]
latest_per_key(rows)   # customer 7 -> new@x.com, customer 9 -> a@y.com

What it tests: a single O(n) pass with a dictionary, instead of sorting or comparing every pair. The trap: comparing timestamps as strings. That works only while every value has the same format and offset. "2026-03-01T23:30:00+05:30" > "2026-03-01T19:00:00+00:00" is True as strings, but the first instant is actually earlier. Parse with datetime.fromisoformat, which understands UTC offsets (Python docs: datetime).

Grouping and aggregating

3. Total per group, without pandas

python
from collections import defaultdict

orders = [("north", 120), ("south", 80), ("north", 45), ("east", 60), ("south", 20)]

totals = defaultdict(int)
for region, amount in orders:
    totals[region] += amount
dict(totals)   # {'north': 165, 'south': 100, 'east': 60}

Follow-up almost every time: "Why not itertools.groupby?" Because it groups consecutive items. The documentation says the input generally needs to be sorted on the same key first (Python docs: itertools). On the list above, unsorted groupby returns five groups, with "north" and "south" twice each. Sort first, and it works, at O(n log n) instead of O(n).

4. Aggregate a CSV that does not fit in memory

python
import csv
from collections import defaultdict

def read_rows(path):
    with open(path, newline="", encoding="utf-8") as f:
        yield from csv.DictReader(f)            # one row at a time

def revenue_by_region(rows):
    totals, skipped = defaultdict(float), 0
    for row in rows:
        if not row["amount"]:                   # blank amount: count it, don't crash
            skipped += 1
            continue
        totals[row["region"]] += float(row["amount"])
    return dict(totals), skipped

# revenue_by_region(read_rows("orders.csv"))
# for a file containing a row with an empty amount: ({'north': 120.5, 'south': 80.0, 'east': 60.0}, 1)

What it tests: streaming with a generator instead of f.readlines() or loading a list, so memory stays flat whatever the file size. Details that score points: opening the file with newline="", as the csv documentation requires (Python docs: csv); counting bad rows instead of silently dropping them; and saying you would log or quarantine them in a real pipeline.

Reshaping and joining

5. Flatten nested JSON into dotted keys

python
def flatten(obj, prefix=""):
    out = {}
    if isinstance(obj, dict):
        for k, v in obj.items():
            out.update(flatten(v, f"{prefix}{k}."))
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            out.update(flatten(v, f"{prefix}{i}."))
    else:
        out[prefix[:-1]] = obj
    return out

event = {"id": 42, "user": {"id": 7, "geo": {"city": "Hyderabad"}},
         "items": [{"sku": "A1", "qty": 2}, {"sku": "B3", "qty": 1}]}
flatten(event)
# {'id': 42, 'user.id': 7, 'user.geo.city': 'Hyderabad',
#  'items.0.sku': 'A1', 'items.0.qty': 2, 'items.1.sku': 'B3', 'items.1.qty': 1}

Follow-ups: "How would you handle the list differently?" Often a list of items should become rows, one per item with the parent id repeated, rather than numbered columns; ask which the consumer needs. "What about very deep nesting?" Recursion depth is limited, so for untrusted input an explicit stack avoids a RecursionError.

6. Join two datasets on a key

python
customers = [{"id": 7, "name": "Asha"}, {"id": 9, "name": "Ravi"}]
orders = [{"order_id": 1, "customer_id": 7},
          {"order_id": 2, "customer_id": 11},       # no such customer
          {"order_id": 3, "customer_id": 9}]

by_id = {c["id"]: c for c in customers}             # build once: O(m)
joined = [{**o, "name": by_id.get(o["customer_id"], {}).get("name")} for o in orders]   # probe: O(n)
# order 2 gets name None: a left join

What it tests: that you build a dictionary on one side (a hash join, O(n + m)) instead of nested loops (O(n × m)). A dictionary lookup is O(1) on average (Python wiki: time complexity). Follow-ups: "Is this an inner or left join?" Left, because unmatched orders are kept with None; filter them out for inner. "What if customer ids are not unique?" The dictionary silently keeps the last one. Map each id to a list instead, or assert uniqueness first. Say which you would do.

Performance, and the bugs interviewers plant

7. "Why is this loop slow?"

python
seen_ids = list(range(100_000))
incoming_ids = [99_999, 5, 123_456]

matches = [e for e in incoming_ids if e in seen_ids]     # scans the list for every check: O(n) each

seen_ids = set(seen_ids)                                 # build the set once
matches = [e for e in incoming_ids if e in seen_ids]     # O(1) average per check
matches                                                  # [99999, 5]

x in list is O(n); x in set is O(1) on average (Python wiki: time complexity). Checking one value at the end of a 100,000-item list is thousands of times slower than the set lookup, and the gap grows with the list. Interviewers want to hear you name the complexity, not just "sets are faster".

8. Spot the bug: mutable defaults and shallow copies

python
def add_tag(record, tags=[]):      # bug: one list shared by every call
    tags.append(record["id"])
    return tags

add_tag({"id": 1})   # [1]
add_tag({"id": 2})   # [1, 2]  <- still holds the first call's tag

def add_tag(record, tags=None):    # fix
    tags = [] if tags is None else tags
    tags.append(record["id"])
    return tags

import copy
template = {"id": None, "tags": []}
row = copy.copy(template)          # shallow: row["tags"] IS template["tags"]
row["tags"].append("vip")
template                           # {'id': None, 'tags': ['vip']}  <- template changed
row = copy.deepcopy(template)      # independent nested objects

Both bugs corrupt data quietly in pipelines. The Python tutorial warns that a default value is evaluated only once, which matters for lists and dictionaries (Python tutorial: default arguments). A shallow copy shares nested objects with the original; a deep copy copies them recursively (Python docs: copy).

9. Sort by team ascending, then salary descending

python
from operator import itemgetter

emps = [("Asha", "data", 72000), ("Ravi", "bi", 65000),
        ("Meena", "data", 90000), ("Kiran", "bi", 65000)]

sorted(emps, key=lambda e: (e[1], -e[2]))      # works because salary is numeric

# General version, for keys you cannot negate: sort in passes, least important first
s = sorted(emps, key=itemgetter(0))                   # name (tie-breaker)
s = sorted(s, key=itemgetter(2), reverse=True)        # salary, descending
s = sorted(s, key=itemgetter(1))                      # team
# [('Kiran','bi',65000), ('Ravi','bi',65000), ('Meena','data',90000), ('Asha','data',72000)]

The multi-pass version relies on Python’s sorts being stable, which the documentation guarantees (Python docs: sorting techniques). Mentioning stability, and why it makes the passes work, is what separates a good answer from a memorised one.

10. Moving average over a stream

python
from collections import deque

def moving_average(stream, n):
    window, total = deque(maxlen=n), 0.0
    for x in stream:
        if len(window) == n:
            total -= window[0]          # value about to fall out of the window
        window.append(x)
        total += x
        yield total / len(window)

list(moving_average([10, 20, 30, 40, 50], 3))   # [10.0, 15.0, 20.0, 30.0, 40.0]

A deque with maxlen drops its oldest item automatically (Python docs: collections), and keeping a running total makes each step O(1) instead of re-summing the window. Follow-up: "What should the first two outputs be?" Here they average fewer than n values. Some teams want None until the window is full; ask.

How to work through a task in the room

  1. Restate the task and ask about the input: size, duplicates, missing values, sort order, types.
  2. Say the approach and its complexity before typing.
  3. Write the simple correct version first; mention the scalable version if the data could be large.
  4. Run through one small example by hand, including an edge case (empty input, a tie, a missing key).
  5. Name what you would add in production: logging bad rows, tests, type hints.

Can I use pandas in a Python coding round for a data role?

Ask. Some interviews allow it; many deliberately do not, to see whether you understand the underlying operations. Being fluent in both is the safe position.

Should I also practise general algorithm problems?

It depends on the company, so ask the recruiter what the round covers. The tasks in this article mirror day-to-day data work (counting, grouping, joining, parsing and streaming), where correctness, data-structure choice and handling messy input matter most.

How should I practise these?

Write each solution from a blank file, time yourself, then change the input: add duplicates, blanks, a mixed timestamp format, a file ten times larger. The follow-ups are about exactly those changes.

Where to go next

The data structures, file handling, generators and standard-library modules used here are all covered in our Python programming course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Interview Questions?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course