Skip to main content

Python for Data Engineering Interviews


Learn how to use Python to build practical, reliable data pipelines. This chapter covers API ingestion, file processing, nested JSON transformation, data quality, aggregation, reusable ETL functions, error handling, retries, and efficient processing at scale.

What you'll learn: Processing API responses, reading and writing files, flattening nested JSON, deduplication and validation, grouping and aggregation, reusable ETL functions, logging and retries, and efficient large-dataset processing.

1. Processing API Responses

Data pipelines often ingest data from REST APIs. A robust client should handle HTTP errors, timeouts, pagination, response parsing, and rate limits rather than assuming every request succeeds.

Make a request and validate the response

import requests

url = "https://api.example.com/orders"

response = requests.get(
    url,
    params={"status": "complete"},
    timeout=20
)
response.raise_for_status()

payload = response.json()
print(payload)

This example uses the third-party requests package. A timeout prevents the client from waiting indefinitely, while raise_for_status() raises an exception for unsuccessful HTTP status codes.

Security note: Use environment variables or a secrets manager for API credentials. Don't hard-code tokens, log authorization headers, or send sensitive data to untrusted endpoints.

Process paginated responses

APIs may return one page at a time. Follow the provider's documented pagination mechanism, which may use a next URL, page number, or cursor.

import requests

def fetch_all_orders(base_url, token):
    headers = {"Authorization": f"Bearer {token}"}
    next_url = base_url
    params = {"limit": 100}

    while next_url:
        response = requests.get(
            next_url,
            headers=headers,
            params=params,
            timeout=20
        )
        response.raise_for_status()
        payload = response.json()

        for order in payload.get("results", []):
            yield order

        # Example cursor-based API response
        next_url = payload.get("next")
        params = None  # next URL already contains pagination details

# for order in fetch_all_orders(api_url, api_token):
#     process_order(order)

Pagination field names and authentication vary by API. Some APIs return a cursor rather than a complete next URL; adapt the loop to the provider's contract.

Handle request failures

import requests

try:
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    data = response.json()
except requests.Timeout:
    print("API request timed out")
except requests.HTTPError as exc:
    print(f"API returned an HTTP error: {exc}")
except requests.RequestException as exc:
    print(f"Request failed: {exc}")
except ValueError as exc:
    print(f"Response was not valid JSON: {exc}")
Interview tip: Discuss pagination, timeouts, rate limits, authentication, schema validation, retryable versus non-retryable errors, and how you avoid ingesting the same page twice.

2. Reading and Writing Files

Python's context manager syntax (with) closes files reliably. Specify an encoding for text and process large inputs incrementally when possible.

Read a file line by line

def read_nonempty_lines(path):
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            line = line.strip()
            if line:
                yield line

for line in read_nonempty_lines("events.log"):
    print(line)

Write output incrementally

def write_lines(path, lines):
    with open(path, "w", encoding="utf-8") as file:
        for line in lines:
            file.write(f"{line}\\n")

write_lines("output.txt", ["row-1", "row-2", "row-3"])

Read and write JSON

import json

with open("input.json", "r", encoding="utf-8") as file:
    records = json.load(file)

with open("output.json", "w", encoding="utf-8") as file:
    json.dump(records, file, indent=2, ensure_ascii=False)

Use json.load() for a file object and json.loads() for a JSON string. A regular JSON document is usually parsed as a whole, so it may not be appropriate for very large files.

Write JSON Lines (JSONL)

import json

records = [
    {"event_id": "e1", "type": "click"},
    {"event_id": "e2", "type": "purchase"}
]

with open("events.jsonl", "w", encoding="utf-8") as file:
    for record in records:
        file.write(json.dumps(record) + "\\n")

JSONL stores one JSON value per line and is convenient for streaming, incremental writes, and many data processing tools.

3. Flattening Nested JSON

API payloads often contain nested dictionaries and arrays. Flattening converts selected nested fields into columns while preserving repeated child entities where necessary.

Extract nested fields

order = {
    "order_id": 1001,
    "customer": {
        "id": "C42",
        "profile": {"city": "Bengaluru"}
    },
    "items": [
        {"sku": "A1", "quantity": 2},
        {"sku": "B7", "quantity": 1}
    ]
}

order_row = {
    "order_id": order["order_id"],
    "customer_id": order["customer"]["id"],
    "customer_city": order["customer"]["profile"]["city"]
}

print(order_row)

Flatten an array into child rows

item_rows = [
    {
        "order_id": order["order_id"],
        "sku": item["sku"],
        "quantity": item["quantity"]
    }
    for item in order.get("items", [])
]

print(item_rows)

This produces one row per item, which is often a better relational model than forcing an array of items into a single column. It also means the order-level fields repeat across child rows.

Use pandas.json_normalize for deeper structures

import pandas as pd

payload = [
    {
        "id": 1,
        "customer": {"name": "Asha", "city": "Pune"},
        "amount": 250
    },
    {
        "id": 2,
        "customer": {"name": "Ravi", "city": "Delhi"},
        "amount": 175
    }
]

df = pd.json_normalize(payload, sep="_")
print(df)
Modeling decision: Flattening every array can multiply rows and distort aggregates. Decide whether an array represents repeated child entities, whether to create a separate child table, and how to preserve parent-child keys.

4. Deduplication and Data Validation

Deduplication starts with a clear business key and a rule for choosing the surviving record. Validation checks whether records meet required structural and business rules.

Deduplicate records by key

records = [
    {"event_id": "e1", "version": 1, "value": "old"},
    {"event_id": "e2", "version": 1, "value": "other"},
    {"event_id": "e1", "version": 2, "value": "latest"}
]

latest_by_id = {}
for record in records:
    event_id = record["event_id"]
    current = latest_by_id.get(event_id)

    if current is None or record["version"] > current["version"]:
        latest_by_id[event_id] = record

deduplicated = list(latest_by_id.values())
print(deduplicated)

This example keeps the highest version per event ID. If the source uses timestamps, define how ties are resolved and what should happen when a timestamp is missing.

Validate required fields and data types

def validate_order(record):
    required = ("order_id", "customer_id", "amount")
    missing = [key for key in required if key not in record]

    if missing:
        return False, f"Missing fields: {missing}"

    if not isinstance(record["amount"], (int, float)):
        return False, "amount must be numeric"

    if record["amount"] < 0:
        return False, "amount cannot be negative"

    return True, None

valid, error = validate_order({
    "order_id": 1001,
    "customer_id": "C42",
    "amount": 250.0
})
Validation detail: In Python, bool is a subclass of int, so the numeric check above also accepts True and False. If booleans must be rejected, explicitly check not isinstance(amount, bool). Consider decimals for precise monetary calculations.

Separate valid and invalid records

valid_records = []
rejected_records = []

for record in records:
    is_valid, reason = validate_order(record)
    if is_valid:
        valid_records.append(record)
    else:
        rejected_records.append({
            "record": record,
            "reason": reason
        })

For production pipelines, persist rejected records with a reason and source metadata so issues can be investigated and replayed.

5. Grouping Records and Aggregations

Grouping is common in ETL jobs: count events, calculate totals, identify the latest event per entity, or prepare summary tables.

Aggregate with defaultdict

from collections import defaultdict

sales_by_region = defaultdict(float)

sales = [
    {"region": "West", "amount": 120.0},
    {"region": "East", "amount": 80.0},
    {"region": "West", "amount": 40.0}
]

for row in sales:
    sales_by_region[row["region"]] += row["amount"]

print(dict(sales_by_region))
# {'West': 160.0, 'East': 80.0}

Count categories with Counter

from collections import Counter

statuses = ["SUCCESS", "FAILED", "SUCCESS", "SUCCESS", "FAILED"]
status_counts = Counter(statuses)

print(status_counts)
print(status_counts.most_common(1))

Group records and compute multiple metrics

from collections import defaultdict

summary = defaultdict(lambda: {"count": 0, "amount": 0.0})

for row in sales:
    region = row["region"]
    summary[region]["count"] += 1
    summary[region]["amount"] += row["amount"]

summary_rows = [
    {"region": region, **metrics}
    for region, metrics in summary.items()
]
print(summary_rows)
Interview tip: If you only need counts, sums, minima, or maxima, maintain running aggregates rather than storing every record in a list. This can reduce memory from O(n) to O(k), where k is the number of groups.

6. Building Reusable ETL Functions

Good ETL code separates extraction, transformation, validation, and loading. Small functions are easier to test and reuse than a single large script.

def extract(source):
    """Read records from a source."""
    return source  # Replace with API, file, or database logic.

def transform(record):
    """Normalize a single record."""
    return {
        "order_id": str(record["order_id"]),
        "amount": float(record["amount"])
    }

def validate(record):
    """Raise an error for invalid business values."""
    if record["amount"] < 0:
        raise ValueError("amount cannot be negative")
    return record

def load(records, target_writer):
    """Send transformed records to a target writer."""
    for record in records:
        target_writer(record)

def run_pipeline(source, target_writer):
    extracted = extract(source)
    transformed = (validate(transform(row)) for row in extracted)
    load(transformed, target_writer)

The code is a structural example. A production implementation should define source and target contracts, batch boundaries, transaction semantics, idempotency, metrics, and failure recovery.

Make transformations testable

def normalize_order(row):
    return {
        "order_id": str(row["order_id"]).strip(),
        "amount": float(row["amount"]),
        "currency": row.get("currency", "USD").upper()
    }

sample = {"order_id": 1001, "amount": "25.50", "currency": "usd"}
expected = {
    "order_id": "1001",
    "amount": 25.5,
    "currency": "USD"
}

assert normalize_order(sample) == expected
Best practice: Keep transformation functions deterministic when possible: the same input should produce the same output. Inject clients, clocks, and writers as dependencies so tests don't require real external services.

7. Logging, Exceptions, and Retries

Logging makes pipeline behavior observable. Exceptions communicate failures, while retries can recover from transient issues such as timeouts or temporary service errors.

Use structured logging basics

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s"
)
logger = logging.getLogger("orders_pipeline")

logger.info("Pipeline started")
logger.warning("Skipping invalid record_id=%s", "e-42")
logger.error("Load failed for batch_id=%s", "batch-7")

Retry transient errors with exponential backoff

import time
import random
import logging

logger = logging.getLogger(__name__)

def retry(operation, attempts=3, base_delay=1.0):
    for attempt in range(1, attempts + 1):
        try:
            return operation()
        except (TimeoutError, ConnectionError):
            if attempt == attempts:
                logger.exception("Operation failed after retries")
                raise

            delay = base_delay * (2 ** (attempt - 1))
            delay += random.uniform(0, 0.25 * delay)
            logger.warning(
                "Transient failure; attempt=%s retrying in %.2fs",
                attempt, delay
            )
            time.sleep(delay)
Retry carefully: Retry only errors that are likely transient. Use a maximum attempt count, backoff, and jitter. Do not blindly retry validation errors or non-idempotent writes; retries can duplicate side effects. Respect API rate-limit responses and provider-specific retry guidance.

Preserve useful exception context

try:
    process_batch(batch)
except ValueError:
    logger.exception("Invalid data in batch_id=%s", batch_id)
    raise
except Exception:
    logger.exception("Unexpected pipeline failure batch_id=%s", batch_id)
    raise

Use logger.exception() inside an exception handler to include traceback details. Re-raise failures when the orchestration layer must mark the task as failed or trigger recovery.

8. Processing Large Datasets Efficiently

Large-data processing is often constrained by memory, network I/O, CPU, or database throughput. Choose the processing pattern based on data size and source capabilities.

Stream records instead of loading everything

import json

def read_jsonl(path):
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            if line.strip():
                yield json.loads(line)

for event in read_jsonl("large_events.jsonl"):
    process_event(event)

Process in batches

from itertools import islice

def batched(iterable, batch_size):
    iterator = iter(iterable)
    while True:
        batch = list(islice(iterator, batch_size))
        if not batch:
            break
        yield batch

for batch in batched(read_jsonl("large_events.jsonl"), 1000):
    process_batch(batch)

Batch size is a tradeoff: small batches reduce peak memory and retry scope but can increase overhead; large batches may improve throughput but require more memory and make retries more expensive.

Use sets for membership and generators for pipelines

valid_customer_ids = {"C1", "C2", "C3"}

def filter_known_customers(rows):
    for row in rows:
        if row.get("customer_id") in valid_customer_ids:
            yield row

filtered = filter_known_customers(read_jsonl("events.jsonl"))

Know when Python is not the right processing engine

  • Use Python for orchestration, API ingestion, moderate transformations, and custom business logic.
  • Use SQL engines for set-based transformations close to the warehouse data.
  • Use distributed engines such as Spark or Apache Beam when data volume or processing requirements exceed a single machine.
  • Use vectorized libraries such as pandas or NumPy for suitable workloads that fit memory and benefit from vectorized operations.
Performance checklist: Avoid unnecessary copies, select only needed columns, stream data, batch I/O, use efficient data structures, measure bottlenecks, and make outputs safe to retry.

Interview Questions to Practice

1. How do you build a reliable API ingestion pipeline?

Use timeouts, authentication through a secrets manager, pagination, schema validation, rate-limit handling, retries for transient errors, observability, and idempotent writes. Track progress so a failed run can resume safely.

2. How do you flatten nested JSON without losing data?

Identify parent and child entities, preserve stable keys, and decide whether arrays should become child rows or separate tables. Validate optional fields and avoid aggregating after a one-to-many expansion without checking row multiplication.

3. How would you deduplicate incoming events?

Define the event's unique key and a deterministic survivor rule, such as highest source sequence or latest valid timestamp. Track duplicates and ensure the target write is idempotent.

4. How do you design reusable ETL code?

Separate extraction, transformation, validation, and loading into small functions. Make dependencies explicit, keep transformations testable, define clear input/output contracts, and expose metrics and failure context.

5. Which errors should be retried?

Usually transient network errors, timeouts, and selected temporary service failures. Do not blindly retry invalid data, authentication failures, or non-idempotent writes. Apply bounded exponential backoff and jitter.

6. How would you process a file too large for memory?

Stream it line by line or in batches, write output incrementally, and avoid collecting every record in a list. If the workload needs distributed computation, use a suitable processing engine instead of forcing it into one Python process.

7. How do you make a pipeline safe to rerun?

Use stable keys, deterministic transformations, idempotent upserts or overwrite-safe partitions, checkpoints, and a clear commit boundary. Ensure partial failures don't cause duplicate records when the job restarts.

Chapter Summary

  • Build API clients with timeouts, pagination, authentication, and deliberate error handling.
  • Read and write files with context managers; stream JSONL when datasets are large.
  • Flatten nested JSON according to its data model, preserving parent-child relationships.
  • Define deduplication keys and validation rules explicitly, and quarantine invalid records when needed.
  • Use running aggregations to limit memory usage when raw group members are unnecessary.
  • Structure ETL into reusable, testable stages and add logging, bounded retries, and idempotent writes.
  • Measure the bottleneck and choose Python, SQL, pandas, Spark, or Beam based on the workload.
Practice challenge: Build a pipeline that fetches paginated order events from an API, flattens customer and item data, validates required fields, deduplicates by event ID, aggregates order totals by customer, and writes JSONL output in batches. Explain your retry policy, logging strategy, and how you would resume after a partial failure.

Note: Examples use Python 3. The API examples use the third-party requests package, and the flattening example uses pandas. Adapt authentication, pagination, schema rules, and retry behavior to the actual service and production environment.

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...