AWS Interview Questions for Data Engineers, Part 2: The boto3 Coding Round

Many AWS data engineering interviews include a short coding round with boto3. The tasks look simple, but each hides a trap: pagination, retries, duplicates, request limits and key ordering. Here are five such tasks, run against a local AWS emulator, with the trap and the answer that shows experience.

GloryTecks Team

Training & Placement Faculty

13 October 2025 7 min read

Last updated: 10 October 2026

Share:

Part 1 of the AWS interview questions for data engineers covers the concept and design questions: storage classes, Glue, Athena, Redshift, streaming and security. Many interviews also include a short coding round. The interviewer shares an editor and asks for a few lines of boto3: count the files, write the output, store an event, clean up a prefix, find the latest export.

These tasks look easy, and each hides a trap that separates people who have run pipelines on AWS from people who have read about it. Below are five of them, run against moto, a library that imitates AWS services locally so boto3 code runs without an account (Moto documentation).

python
import os
os.environ.update(AWS_ACCESS_KEY_ID="testing", AWS_SECRET_ACCESS_KEY="testing", AWS_DEFAULT_REGION="ap-south-1")
import boto3
from botocore.exceptions import ClientError
from moto import mock_aws

with mock_aws():                                     # local AWS emulator: no real account, no cost
    s3 = boto3.client("s3", region_name="ap-south-1")
    s3.create_bucket(Bucket="lake", CreateBucketConfiguration={"LocationConstraint": "ap-south-1"})
    for i in range(2500):
        s3.put_object(Bucket="lake", Key=f"raw/orders/part-{i:05d}.json", Body=b"{}")

    # Task 1: "How many files are under raw/orders/?"
    first_call = s3.list_objects_v2(Bucket="lake", Prefix="raw/orders/")
    pages = s3.get_paginator("list_objects_v2").paginate(Bucket="lake", Prefix="raw/orders/")
    total = sum(page["KeyCount"] for page in pages)
    print(f"T1 one list_objects_v2 call: {first_call['KeyCount']} keys, IsTruncated={first_call['IsTruncated']} | paginator: {total} keys")

    # Task 2: "Make the daily job safe to retry"
    def run_job(day, run_id, deterministic):
        key = f"curated/orders/day={day}/part-0.json" if deterministic else f"curated/orders/day={day}/{run_id}.json"
        s3.put_object(Bucket="lake", Key=key, Body=b'{"rows": 120}')
    for deterministic in (False, True):
        for run_id in ("run-a", "run-b-retry"):              # the scheduler retried the same day
            run_job("2026-10-09" if deterministic else "2026-10-08", run_id, deterministic)
    for day, label in (("2026-10-08", "key per run   "), ("2026-10-09", "key per day   ")):
        n = s3.list_objects_v2(Bucket="lake", Prefix=f"curated/orders/day={day}/")["KeyCount"]
        print(f"T2 {label}: {n} output file(s) after one retry")

    # Task 3: "Record each payment event exactly once"
    ddb = boto3.client("dynamodb", region_name="ap-south-1")
    ddb.create_table(TableName="payments", KeySchema=[{"AttributeName": "event_id", "KeyType": "HASH"}],
                     AttributeDefinitions=[{"AttributeName": "event_id", "AttributeType": "S"}], BillingMode="PAY_PER_REQUEST")
    outcomes = []
    for attempt in range(2):                                  # the same event delivered twice
        try:
            ddb.put_item(TableName="payments", Item={"event_id": {"S": "evt-981"}, "amount": {"N": "499"}},
                         ConditionExpression="attribute_not_exists(event_id)")
            outcomes.append("written")
        except ClientError as err:
            outcomes.append(err.response["Error"]["Code"])
    print(f"T3 conditional put, delivered twice: {outcomes} | items stored: {ddb.scan(TableName='payments')['Count']}")

    # Task 4: "Delete everything under raw/orders/"
    requests = 0
    for page in s3.get_paginator("list_objects_v2").paginate(Bucket="lake", Prefix="raw/orders/"):
        batch = [{"Key": o["Key"]} for o in page.get("Contents", [])]   # each page holds at most 1,000 keys
        if batch:
            s3.delete_objects(Bucket="lake", Delete={"Objects": batch}); requests += 1
    left = s3.list_objects_v2(Bucket="lake", Prefix="raw/orders/")["KeyCount"]
    print(f"T4 delete page by page: {requests} delete_objects requests, {left} keys left")
    for i in range(1500):
        s3.put_object(Bucket="lake", Key=f"tmp/{i:05d}", Body=b"")
    too_many = [{"Key": f"tmp/{i:05d}"} for i in range(1500)]
    try:
        s3.delete_objects(Bucket="lake", Delete={"Objects": too_many})
        print("   the emulator also accepted one request with 1,500 keys")
    except ClientError as err:
        print(f"   one request with 1,500 keys: {err.response['Error']['Code']}")

    # Task 5: "Find the latest daily file without reading them all"
    for day in (8, 9, 10, 11):
        s3.put_object(Bucket="lake", Key=f"exports/day={day}/data.csv", Body=b"x")
        s3.put_object(Bucket="lake", Key=f"exports2/day=2026-10-{day:02d}/data.csv", Body=b"x")
    for prefix in ("exports/", "exports2/"):
        listed = [o["Key"] for o in s3.list_objects_v2(Bucket="lake", Prefix=prefix)["Contents"]]
        print(f"T5 keys under {prefix:<9} listed in order: {[k.split('/')[1] for k in listed]} -> last is {listed[-1].split('/')[1]}")
text
T1 one list_objects_v2 call: 1000 keys, IsTruncated=True | paginator: 2500 keys
T2 key per run   : 2 output file(s) after one retry
T2 key per day   : 1 output file(s) after one retry
T3 conditional put, delivered twice: ['written', 'ConditionalCheckFailedException'] | items stored: 1
T4 delete page by page: 3 delete_objects requests, 0 keys left
   the emulator also accepted one request with 1,500 keys
T5 keys under exports/  listed in order: ['day=10', 'day=11', 'day=8', 'day=9'] -> last is day=9
T5 keys under exports2/ listed in order: ['day=2026-10-08', 'day=2026-10-09', 'day=2026-10-10', 'day=2026-10-11'] -> last is day=2026-10-11

Task 1: "How many files are under this prefix?"

The trap. One call to list_objects_v2 returned 1,000 keys and IsTruncated=True, although there were 2,500. A ListObjectsV2 request returns at most 1,000 objects, and a truncated response comes with a token to continue (S3: ListObjectsV2). Code that reads one response silently undercounts, and in a pipeline that means silently skipping files.

The answer. Use the paginator, as the code does, and sum across pages. Say out loud why: the limit is per request, and production prefixes grow past it without warning. A strong follow-up is to mention that for millions of objects, listing is slow and costly, and S3 Inventory or a manifest written by the producer is the better source.

Task 2: "Make the daily job safe to retry"

The trap. Writing output to a key that includes a run ID or timestamp means a retried run adds a second file. In the demo, the scheduler retried one day, and the key-per-run version left two output files: a downstream query would count that day twice.

The answer. Derive the output key from the data, not the run: one deterministic key or prefix per partition, such as the day. A retry then overwrites the same object, and the day still has one file. For multi-file outputs, write to a temporary prefix and replace the partition in one step at the end, or record a manifest of the files that make up the partition.

Task 3: "Record each payment event exactly once"

The trap. Queues and streams can deliver the same event more than once. A plain put_item overwrites by key, which hides duplicates but also loses the information that a duplicate arrived, and with a generated key it stores two items.

The answer. Use the event's own ID as the key and a condition expression, attribute_not_exists(event_id). DynamoDB then rejects a write when an item with that key already exists (DynamoDB: condition expressions). In the run, the second delivery raised ConditionalCheckFailedException, one item was stored, and the code can count or log the duplicate. Catch that specific error, not every ClientError.

Task 4: "Delete everything under a prefix"

The trap. delete_objects takes up to 1,000 keys per request (S3: DeleteObjects). Passing a full list of 2,500 keys fails on real S3.

The answer. Delete page by page: each listing page holds at most 1,000 keys, so each page becomes one delete request. The demo needed three requests. Also check the response's Errors list: the call succeeds as a whole even when individual keys fail, for example for lack of permission.

The emulator lesson. Moto accepted one request with 1,500 keys, which real S3 would reject. Emulators are excellent for fast tests, but they do not enforce every service limit. Saying this in an interview shows you know where local tests stop and an integration test against a real sandbox account begins.

Task 5: "Find the latest daily export"

The trap. S3 lists keys of a general purpose bucket in lexicographic order. With keys like day=8 and day=10, the text order is 10, 11, 8, 9, so "take the last key" returned day 9 instead of day 11.

The answer. Design keys so text order matches time order: zero-padded ISO dates such as day=2026-10-11. With those, the last listed key was the latest. Better still, avoid listing at all: have the producer write a small _latest marker or manifest, or query a catalogue. Note that directory buckets, the S3 Express One Zone type, do not return keys in lexicographic order, so this trick does not apply there.

How to behave in the coding round

  • Ask about scale first: "How many objects?" turns Task 1 from trivial into the real question.
  • Name the failure mode before writing code: retries, duplicates and limits are what is being tested.
  • Handle the specific error, not every exception.
  • Mention how you would test it: an emulator for logic, a real sandbox account for limits and permissions.

For the pipeline design that these tasks belong to, see building a data lake on S3 and Glue, and for the IAM roles the code would run under, IAM explained.

Why does list_objects_v2 return only 1,000 files?

Each ListObjectsV2 request returns at most 1,000 objects. If IsTruncated is true, continue with the continuation token, or use a boto3 paginator, which does this for you.

How do I prevent duplicate writes to DynamoDB?

Use the event's own ID as the key and add the condition attribute_not_exists on it. DynamoDB rejects the second write with a conditional check failure, which you can catch and log.

How many objects can delete_objects remove at once?

Up to 1,000 keys per request. Split larger deletions into batches and check the Errors list in each response.

Is moto good enough to test AWS code?

It is good for fast tests of logic, but it does not enforce every AWS limit. Run integration tests against a real sandbox account before relying on limits and permissions.

AWS pipelines, from S3 layouts to idempotent jobs, are built step by step in the Data Engineering 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 AWS?

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

Explore course