Skip to main content

Intermediate Python for Data Engineering Interviews


Move beyond Python basics with practical techniques for transforming collections, grouping records, processing streams, parsing text and nested data, and reasoning about performance. These patterns appear frequently in data engineering scripts, ETL jobs, and coding interviews.

What you'll learn: Dictionary and list manipulation, sorting and grouping, Counter/defaultdict/deque, iterators and generators, string processing, CSV/JSON and nested data, and time and space complexity.

1. Dictionary and List Manipulation

Lists are useful for ordered sequences, while dictionaries provide key-based lookups. In data engineering, you often transform a list of records, build lookup maps, and aggregate fields.

Transform a list of dictionaries

orders = [
    {"order_id": 101, "customer_id": "C1", "amount": 250},
    {"order_id": 102, "customer_id": "C2", "amount": 175},
    {"order_id": 103, "customer_id": "C1", "amount": 300},
]

# Select only the fields needed downstream
rows = [
    {"id": row["order_id"], "amount": row["amount"]}
    for row in orders
]

print(rows)

Build a lookup dictionary

customer_names = {
    "C1": "Asha",
    "C2": "Ravi",
}

for order in orders:
    order["customer_name"] = customer_names.get(
        order["customer_id"], "UNKNOWN"
    )

print(orders)

Aggregate values by key

total_by_customer = {}

for order in orders:
    customer_id = order["customer_id"]
    total_by_customer[customer_id] = (
        total_by_customer.get(customer_id, 0) + order["amount"]
    )

print(total_by_customer)
# {'C1': 550, 'C2': 175}

Safely update nested dictionaries

record = {
    "customer": {
        "id": "C1",
        "address": {"city": "Bengaluru"}
    }
}

city = record.get("customer", {}).get("address", {}).get("city")
print(city)  # Bengaluru
Common pitfall: dict.get() helps when a key may be absent, but chained get() calls do not protect you if an intermediate value exists but is None or has the wrong type. Validate incoming records when schemas can vary.

Shallow copy versus deep copy

import copy

original = {"tags": ["new", "priority"]}
shallow = original.copy()
deep = copy.deepcopy(original)

original["tags"].append("processed")

print(shallow["tags"])  # shares the nested list
print(deep["tags"])     # independent nested list

A shallow copy creates a new outer collection but retains references to nested objects. Use a deep copy only when you need independent nested structures and can afford its cost.

2. Sorting and Grouping

Sort records by one or more fields

employees = [
    {"name": "Mina", "team": "Data", "salary": 120000},
    {"name": "Asha", "team": "Platform", "salary": 135000},
    {"name": "Ravi", "team": "Data", "salary": 110000},
]

# Sort by salary descending
by_salary = sorted(
    employees,
    key=lambda employee: employee["salary"],
    reverse=True
)

# Sort by team, then salary descending
by_team_salary = sorted(
    employees,
    key=lambda employee: (employee["team"], -employee["salary"])
)

print(by_team_salary)

sorted() returns a new list; list.sort() sorts a list in place and returns None. Python sorting is stable, so equal keys preserve their prior relative order.

Group records with itertools.groupby

from itertools import groupby

# groupby groups consecutive matching keys,
# so sort by the grouping key first.
employees_sorted = sorted(employees, key=lambda row: row["team"])

for team, rows in groupby(employees_sorted, key=lambda row: row["team"]):
    team_rows = list(rows)
    print(team, len(team_rows), team_rows)
Important: itertools.groupby() does not automatically collect all equal keys from an unsorted list. It groups adjacent runs. Sort by the same key first, or use a dictionary-based aggregation when order is not important.

Group and aggregate without groupby

from collections import defaultdict

orders_by_customer = defaultdict(list)

for order in orders:
    orders_by_customer[order["customer_id"]].append(order)

for customer_id, customer_orders in orders_by_customer.items():
    total = sum(order["amount"] for order in customer_orders)
    print(customer_id, total)
Interview tip: Clarify whether the output needs to preserve order, whether the input can be modified, and how ties should be handled. These details often change the best solution.

3. Collections: Counter, defaultdict, and deque

The collections module provides efficient, purpose-built data structures for common tasks.

Counter: count values

from collections import Counter

statuses = ["SUCCESS", "FAILED", "SUCCESS", "SKIPPED", "FAILED", "SUCCESS"]
counts = Counter(statuses)

print(counts)
print(counts.most_common(2))
print(counts["SUCCESS"])  # 3

Use Counter for frequency counts, duplicate detection, and top-N values.

defaultdict: build grouped collections

from collections import defaultdict

events_by_user = defaultdict(list)

events = [
    ("U1", "login"),
    ("U2", "login"),
    ("U1", "purchase"),
]

for user_id, event_type in events:
    events_by_user[user_id].append(event_type)

print(dict(events_by_user))

A defaultdict(list) creates an empty list automatically when a missing key is first accessed. Note that accessing a missing key creates it.

deque: efficient queue operations

from collections import deque

queue = deque(["file1.csv", "file2.csv"])
queue.append("file3.csv")      # add to right
queue.appendleft("priority.csv")
next_file = queue.popleft()     # remove from left

print(next_file)
print(queue)

Appending and popping at either end of a deque are approximately O(1). Removing the first item from a regular list with pop(0) is O(n), because remaining elements must shift.

Maintain a rolling window

from collections import deque

window = deque(maxlen=3)

for value in [10, 20, 30, 40]:
    window.append(value)
    print(list(window))

With maxlen, appending to a full deque automatically discards an item from the opposite end. This is useful for bounded rolling buffers, but calculate rolling aggregates carefully if values leave the window.

4. Iterators and Generators

An iterable can produce an iterator. An iterator yields one item at a time using next(). A generator is a convenient way to create an iterator using yield.

Iterator basics

values = [10, 20, 30]
iterator = iter(values)

print(next(iterator))  # 10
print(next(iterator))  # 20
print(next(iterator))  # 30
# Another next(iterator) raises StopIteration

Create a generator with yield

def read_batches(records, batch_size):
    for start in range(0, len(records), batch_size):
        yield records[start:start + batch_size]

records = list(range(1, 11))

for batch in read_batches(records, 3):
    print(batch)

This example slices a list into batches. For a truly large or streaming source, iterate over the input incrementally instead of requiring the full list in memory.

Stream records from an input iterator

def positive_amounts(rows):
    for row in rows:
        amount = row.get("amount")
        if amount is not None and amount > 0:
            yield row

# The source can itself be a generator or file iterator.
valid_rows = positive_amounts(orders)

for row in valid_rows:
    print(row)
Why generators matter: They can reduce peak memory by producing results on demand. They do not make every operation faster, and a generator is typically exhausted after one pass.

Generator expression versus list comprehension

numbers = range(1_000_000)

# Builds a list in memory
squares_list = [n * n for n in numbers]

# Produces values on demand
squares_generator = (n * n for n in numbers)

# Both can be consumed by sum()
total = sum(n * n for n in range(100))

5. String Processing

Text cleanup and parsing are common in ingestion pipelines, log processing, and data normalization.

Trim, split, join, and replace

raw = "  Alice Smith, Bengaluru, IN  "

clean = raw.strip()
parts = [part.strip() for part in clean.split(",")]
normalized = " | ".join(parts)

print(parts)
print(normalized)
print(clean.replace("Bengaluru", "Bangalore"))

Check prefixes, suffixes, and case

filename = "ORDERS_2026.CSV"

print(filename.lower().endswith(".csv"))  # True
print(filename.startswith("ORDERS"))      # True
print("failed".upper())                   # FAILED

Extract patterns with regular expressions

import re

text = "Job run_id=run-482 status=FAILED duration=31s"
match = re.search(r"run_id=(\\S+).*status=(\\w+)", text)

if match:
    run_id, status = match.groups()
    print(run_id, status)

Use regular expressions for pattern-based parsing, but prefer straightforward string operations for simple, fixed delimiters. For complex formats such as CSV, use a CSV parser rather than splitting on commas manually.

Normalize whitespace and compare text

def normalize_name(value):
    if value is None:
        return None
    return " ".join(value.split()).casefold()

print(normalize_name("  Asha   KUMAR "))  # asha kumar

casefold() is intended for caseless matching and handles some Unicode cases more thoroughly than lower(). Whether normalization is appropriate depends on the field and business rules.

6. Working with CSV, JSON, and Nested Data

Read CSV safely

import csv

with open("orders.csv", "r", newline="", encoding="utf-8") as file:
    reader = csv.DictReader(file)
    for row in reader:
        order_id = row["order_id"]  # CSV values are strings
        amount = float(row["amount"])
        print(order_id, amount)

CSV values are usually strings, so convert them deliberately and validate empty or malformed fields. Use newline="" as recommended by Python's CSV module.

Write CSV records

import csv

rows = [
    {"order_id": 101, "amount": 250.0},
    {"order_id": 102, "amount": 175.0},
]

with open("output.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.DictWriter(file, fieldnames=["order_id", "amount"])
    writer.writeheader()
    writer.writerows(rows)

Parse JSON and navigate nested data

import json

payload = {
    "event_id": "e-101",
    "user": {"id": "u-7", "location": {"city": "Pune"}},
    "items": [
        {"sku": "A1", "quantity": 2},
        {"sku": "B2", "quantity": 1}
    ]
}

text = json.dumps(payload)
event = json.loads(text)

user_id = event["user"]["id"]
city = event["user"]["location"]["city"]
skus = [item["sku"] for item in event["items"]]

print(user_id, city, skus)

Flatten nested records

flattened = {
    "event_id": event["event_id"],
    "user_id": event["user"]["id"],
    "city": event["user"]["location"]["city"],
    "item_count": len(event["items"]),
    "total_quantity": sum(item["quantity"] for item in event["items"])
}

print(flattened)
Production caution: Nested input may omit keys, contain null values, or change schema over time. Validate expected structure and decide whether invalid records should be rejected, quarantined, or handled with defaults. Avoid silently substituting defaults when that could corrupt business meaning.

Process JSON Lines incrementally

import json

def read_jsonl(path):
    with open(path, "r", encoding="utf-8") as file:
        for line_number, line in enumerate(file, start=1):
            if not line.strip():
                continue
            try:
                yield json.loads(line)
            except json.JSONDecodeError as exc:
                # Replace with structured logging/quarantine in production.
                print(f"Invalid JSON at line {line_number}: {exc}")

for event in read_jsonl("events.jsonl"):
    print(event.get("event_id"))

7. Time and Space Complexity

Big O notation describes how an algorithm's resource needs grow with input size. It helps compare approaches before running them on production-scale data.

OperationTypical complexityNotes
List index accessO(1)Direct access by position
Search in a listO(n)May inspect every element
Append to a listAmortized O(1)Occasional resizing can cost more
Insert/remove at list frontO(n)Elements shift positions
Dictionary lookup/insertAverage O(1)Worst-case behavior can differ
Set membershipAverage O(1)Hash-based lookup
Comparison sortO(n log n)Python sorting is stable
Build a list of n itemsO(n) spaceStores all items in memory
Generator pipelineOften O(1) extra spaceDepends on upstream and downstream operations

Example: List membership versus set membership

allowed_ids_list = list(range(100_000))
allowed_ids_set = set(allowed_ids_list)

target = 99_999

# List search is O(n) in the worst case
found_in_list = target in allowed_ids_list

# Set membership is average O(1)
found_in_set = target in allowed_ids_set

Building the set costs additional time and memory, so it is most useful when you perform many membership checks or need uniqueness.

Avoid accidental quadratic behavior

# Less efficient: repeated list membership can be O(n*m)
unique = []
for value in values:
    if value not in unique:
        unique.append(value)

# Often more efficient for membership, while preserving first-seen order
seen = set()
unique = []
for value in values:
    if value not in seen:
        seen.add(value)
        unique.append(value)
Interview tip: State both time and space complexity. Explain assumptions, such as average-case hash-table operations, whether output order must be preserved, and whether the input can be modified.

Interview Questions to Practice

1. How do you group a list of dictionaries by a key?

Use defaultdict(list) to append records under each key, or use a dictionary and initialize missing keys. For aggregations such as sums or counts, store the running aggregate instead of every record if the original rows are not needed.

2. What is the difference between sorted() and list.sort()?

sorted() returns a new sorted list from an iterable. list.sort() mutates an existing list in place and returns None. Both use a stable sort.

3. How is itertools.groupby different from SQL GROUP BY?

itertools.groupby() groups consecutive runs of equal keys, so data usually needs sorting by the grouping key first. SQL GROUP BY groups matching keys across the input regardless of their original order.

4. Why use deque instead of a list for a queue?

A deque supports efficient appends and pops at both ends. Removing index zero from a list is O(n) because the remaining elements must shift.

5. What is the benefit of generators in ETL pipelines?

Generators yield records on demand and can reduce peak memory. They are useful for streaming transformations, but a generator is normally consumed once and doesn't eliminate memory used by other pipeline stages.

6. How do you process a large CSV or JSONL file?

Use the CSV reader or iterate over JSONL one line at a time. Validate and transform each record incrementally, handle malformed rows explicitly, and write output in batches or as a stream where possible.

7. How can you improve repeated membership checks?

Convert the lookup collection to a set when appropriate. Set construction costs O(n) time and memory, but repeated membership checks are average O(1), compared with O(n) per search in a list.

Chapter Summary

  • Use lists and dictionaries to transform records and build efficient lookup or aggregation structures.
  • Understand sorting stability and remember that itertools.groupby() groups consecutive keys.
  • Use Counter for frequencies, defaultdict for grouped values, and deque for queues or rolling buffers.
  • Use generators to process records lazily and reduce memory pressure where the full result need not be retained.
  • Use standard CSV and JSON parsers rather than manually splitting structured formats.
  • Analyze both time and space complexity, and measure performance with realistic data volumes.
Practice challenge: Read a large CSV of event records, normalize user names, discard invalid rows, group events by user, count event types, and emit JSON summaries. Explain how you would avoid loading all records into memory and discuss the time and space complexity of your solution.

Note: Examples use Python 3. Adapt parsing, validation, logging, and memory-management choices to your data volume and production requirements.

Comments

Popular posts from this blog

Tricky Questions or Puzzles in C ( Updated for 2026)

Updated for 2026 This article was originally written when C/C++ puzzles were commonly asked in interviews. While such language-specific puzzles are less frequent today, the problem-solving and logical reasoning skills tested here remain highly relevant for modern Software Engineering, Data Engineering, SQL, and system design interviews . Why These Puzzles Still Matter in 2026 Although most Software &   Data Engineering interviews today focus on Programming, SQL, data pipelines, cloud platforms, and system design , interviewers still care deeply about how you think . These puzzles test: Logical reasoning Edge-case handling Understanding of execution flow Ability to reason under pressure The language may change , but the thinking patterns do not . How These Skills Apply to Data Engineering Interviews The same skills tested by C/C++ puzzles appear in modern interviews as: SQL edge cases and NULL handling Data pipeline failure scenarios Incremental vs ...

Programs and Puzzles in technical interviews i faced

I have attended interview of nearly 10 companies in my campus placements and sharing their experiences with you,though i did not got selected in any of the companies but i had great experience facing their interviews and it might help you as well in preparation of interviews.Here are some of the puzzles and programs asked to me in interview in some of the good companies. 1) SAP Labs I attended sap lab online test in my college through campus placements.It had 3 sections,the first one is usual aptitude questions which i would say were little tricky to solve.The second section was Programming test in which you were provided snippet of code and you have to complete the code (See Tricky Code Snippets  ).The code are from different data structures like Binary Tree, AVL Tree etc.Then the third section had questions from Database,OS and Networks.After 2-3 hours we got the result and i was shortlisted for the nest round of interviews scheduled next day.Then the next day we had PPT of t...

Decorators in Python

Decorators provide additional functionality to the functions without directly changing their definition of them. Basically, it takes the functions as an argument, adds functionality to them, and returns it.  Before diving deep into the concept of Decorators, let's first try to understand  What are functions in Python and what is an inner function? In Python everything is Objects, be it Class, Variables, and Functions . So functions are python first-class objects that can be used or passed as an argument. You can store the functions in variables, you can pass a function to another function as parameters, and you can also return the function from the function. Below is one simple example where we are treating functions as objects . def make_me_lowercase ( str ): return str .lower() print (make_me_lowercase( "HELLO World" )) copy_of_you = make_me_lowercase print (copy_of_you( "HELLO World" )) The output of the above calls for both the functio...