Skip to content

Lists and Dictionaries

Lists and dictionaries are Python's two most important built-in data structures for analytics. Before Pandas DataFrames, you manipulate data using these. Even after, you use them constantly — for configurations, lookup tables, grouping results, and building data pipelines. Mastery of lists and dicts is the difference between writing readable Python and fighting the language.

Learning Objectives

  • Create and manipulate lists: indexing, slicing, appending, sorting
  • Create and work with dictionaries: access, update, iteration, nested structures
  • Use sets for deduplication and fast membership testing
  • Understand tuples and when to prefer them over lists
  • Apply list and dict comprehensions to transform data compactly
  • Represent tabular data as a list of dicts

Lists

A list is an ordered, mutable collection. Elements can be of any type and can repeat.

# Creating lists
revenues   = [42_000, 38_500, 51_200, 49_800, 55_100]
months     = ["Jan", "Feb", "Mar", "Apr", "May"]
mixed      = [1, "hello", 3.14, True, None]    # valid — but unusual in practice
empty      = []

# Confirm type and length
print(type(revenues))    # <class 'list'>
print(len(revenues))     # 5

Indexing and slicing

Python uses zero-based indexing. Slicing uses [start:stop:step] where stop is excluded.

revenues = [42_000, 38_500, 51_200, 49_800, 55_100]
#  index:       0       1       2       3       4
# -index:      -5      -4      -3      -2      -1

# Single element
print(revenues[0])    # 42000  — first element
print(revenues[-1])   # 55100  — last element (no need to know length)
print(revenues[-2])   # 49800  — second from last

# Slices [start:stop:step] — stop is exclusive
print(revenues[1:4])   # [38500, 51200, 49800]  — indices 1, 2, 3
print(revenues[:3])    # [42000, 38500, 51200]   — first 3
print(revenues[2:])    # [51200, 49800, 55100]   — from index 2 to end
print(revenues[::2])   # [42000, 51200, 55100]   — every 2nd element
print(revenues[::-1])  # [55100, 49800, 51200, 38500, 42000]  — reversed copy

# Slicing never raises IndexError
print(revenues[10:20])  # []  — out-of-range slice is just empty

Modifying lists

revenues = [42_000, 38_500, 51_200, 49_800, 55_100]

# Add elements
revenues.append(48_250)             # add to end — O(1)
revenues.insert(0, 35_000)          # insert at position 0 — O(n), shifts everything
revenues.extend([52_000, 60_000])   # add multiple items — modifies in place

# Remove elements
revenues.remove(38_500)             # removes first occurrence of VALUE (ValueError if missing)
popped      = revenues.pop()        # remove and return last element
popped_at_0 = revenues.pop(0)       # remove and return element at index 0

# Replace
revenues[1] = 40_000                # direct assignment

# Sort
revenues.sort()                     # sort in place, ascending
revenues.sort(reverse=True)        # sort in place, descending
sorted_copy = sorted(revenues)     # sorted() returns NEW list — original unchanged

# Reverse
revenues.reverse()                  # reverses in place

list.sort() vs sorted()

list.sort() modifies the list in place and returns None. A common mistake is revenues = revenues.sort() — this sets revenues to None. Use sorted(revenues) when you need a new sorted list without changing the original.

Useful list operations

revenues = [42_000, 38_500, 51_200, 49_800, 55_100]

print(len(revenues))              # 5
print(sum(revenues))              # 236600
print(max(revenues))              # 55100
print(min(revenues))              # 38500
print(42_000 in revenues)         # True  — membership check
print(99_000 in revenues)         # False
print(revenues.count(42_000))     # 1  — how many times does 42000 appear?
print(revenues.index(51_200))     # 2  — index of first occurrence (ValueError if missing)

# Copy a list (to avoid modifying the original)
copy = revenues.copy()            # shallow copy — correct for flat lists
copy2 = revenues[:]               # same effect — slice of entire list
import copy as copymod
deep_copy = copymod.deepcopy(revenues)  # deep copy — needed for nested lists

Lists of dictionaries — the analytics pattern

Before DataFrames, tabular data lives as a list of dicts. Each dict is a row; keys are column names.

orders = [
    {"order_id": "O001", "customer": "Priya",  "total": 298.00, "status": "completed"},
    {"order_id": "O002", "customer": "Sarah",  "total": 389.00, "status": "completed"},
    {"order_id": "O003", "customer": "Arjun",  "total": 149.97, "status": "pending"},
    {"order_id": "O004", "customer": "Priya",  "total": 35.99,  "status": "completed"},
    {"order_id": "O005", "customer": "Sarah",  "total": 89.50,  "status": "cancelled"},
]

# Filter completed orders
completed = [o for o in orders if o["status"] == "completed"]
print(f"Completed: {len(completed)} orders")   # 3

# Total revenue from completed
total_rev = sum(o["total"] for o in completed)
print(f"Total: £{total_rev:,.2f}")   # £722.99

# Sort by total descending
sorted_orders = sorted(orders, key=lambda o: o["total"], reverse=True)
for o in sorted_orders:
    print(f"  {o['order_id']}: £{o['total']:.2f} ({o['status']})")

# Find orders for a specific customer
priya_orders = [o for o in orders if o["customer"] == "Priya"]
priya_total  = sum(o["total"] for o in priya_orders if o["status"] == "completed")
print(f"Priya completed: £{priya_total:,.2f}")

Dictionaries

A dictionary maps keys to values. Keys must be unique and immutable (strings, numbers, tuples). Values can be anything.

# Creating a dictionary
customer = {
    "id":           "C001",
    "name":         "Priya Sharma",
    "city":         "Mumbai",
    "total_orders": 9,
    "total_spend":  1456.25,
    "is_vip":       True,
    "email":        None,        # field exists but value is unknown
}

Accessing and modifying

# Access by key
print(customer["name"])                          # Priya Sharma
print(customer.get("email"))                     # None  — key exists, value is None
print(customer.get("phone"))                     # None  — key does not exist, no error
print(customer.get("phone", "not provided"))     # not provided  — with default

# Modify
customer["total_orders"] += 1                   # 10
customer["segment"] = "VIP"                     # add a new key

# Delete
del customer["city"]
removed = customer.pop("is_vip")                # removes and returns value: True
customer.pop("missing_key", None)               # safe pop — no error if key absent

# Check existence
print("name" in customer)           # True
print("phone" in customer)          # False
print("name" not in customer)       # False

Iterating over dictionaries

# Keys only
for key in customer:
    print(key)

# Values only
for val in customer.values():
    print(val)

# Keys and values together — most common pattern
for key, val in customer.items():
    print(f"  {key}: {val}")

# Output:
# id: C001
# name: Priya Sharma
# total_orders: 10
# total_spend: 1456.25
# segment: VIP
# email: None

Dictionary comprehension

products = {"headphones": 149, "yoga_mat": 36, "desk": 389, "analytics_book": 50}

# Filter: only products over £100
premium = {name: price for name, price in products.items() if price > 100}
print(premium)   # {'headphones': 149, 'desk': 389}

# Transform: apply a 10% discount to all
discounted = {name: round(price * 0.9, 2) for name, price in products.items()}
print(discounted)   # {'headphones': 134.1, 'yoga_mat': 32.4, 'desk': 350.1, 'analytics_book': 45.0}

# Invert a dictionary (swap keys and values)
id_to_name = {"C001": "Priya", "C002": "Sarah", "C003": "Arjun"}
name_to_id = {v: k for k, v in id_to_name.items()}
print(name_to_id)   # {'Priya': 'C001', 'Sarah': 'C002', 'Arjun': 'C003'}

Nested dictionaries

monthly_data = {
    "Jan": {"revenue": 42_000, "orders": 180, "customers": 145},
    "Feb": {"revenue": 38_500, "orders": 162, "customers": 130},
    "Mar": {"revenue": 51_200, "orders": 215, "customers": 178},
}

# Access nested value
print(monthly_data["Mar"]["revenue"])    # 51200

# Iterate and compute derived metrics
for month, data in monthly_data.items():
    avg_order = data["revenue"] / data["orders"]
    print(f"  {month}: £{avg_order:.2f} avg order value, {data['customers']} customers")

# Add a computed field to each nested dict
for month, data in monthly_data.items():
    data["revenue_per_customer"] = round(data["revenue"] / data["customers"], 2)

Building a group-by with defaultdict

The defaultdict from collections is the standard tool for accumulating grouped data.

from collections import defaultdict

orders = [
    {"customer": "C001", "total": 298.00, "status": "completed"},
    {"customer": "C001", "total": 35.99,  "status": "completed"},
    {"customer": "C002", "total": 149.97, "status": "pending"},
    {"customer": "C003", "total": 389.00, "status": "completed"},
    {"customer": "C002", "total": 89.50,  "status": "completed"},
]

# Group by customer — list of orders per customer
by_customer = defaultdict(list)
for order in orders:
    by_customer[order["customer"]].append(order)

# Summarise each customer
for customer, customer_orders in sorted(by_customer.items()):
    completed = [o for o in customer_orders if o["status"] == "completed"]
    total_spend = sum(o["total"] for o in completed)
    print(f"  {customer}: {len(completed)} completed orders, £{total_spend:,.2f} spent")

# Output:
# C001: 2 completed orders, £333.99 spent
# C002: 1 completed orders, £89.50 spent
# C003: 1 completed orders, £389.00 spent

For mid-level analysts

defaultdict(list) removes the boilerplate of checking if a key exists before appending. defaultdict(float) does the same for running sums. It is one of the most useful standard library tools for analytics work without Pandas.


Sets — Unique Values and Membership

A set is an unordered collection of unique values. It excels at two tasks: deduplication and fast membership testing.

# Deduplication — fastest way to find unique values
all_customers = ["C001", "C002", "C001", "C003", "C002", "C004"]
unique = set(all_customers)
print(unique)               # {'C001', 'C002', 'C003', 'C004'} — order not guaranteed
print(len(unique))          # 4

# Membership test — O(1) vs O(n) for a list
vip_ids = {"C001", "C003", "C012"}
print("C001" in vip_ids)    # True  — very fast even with millions of elements

# Set operations — very useful for cohort analysis
jan_buyers = {"C001", "C002", "C003"}
feb_buyers = {"C002", "C003", "C004"}

retained    = jan_buyers & feb_buyers    # intersection — bought both months
all_buyers  = jan_buyers | feb_buyers    # union
jan_only    = jan_buyers - feb_buyers    # churned — only January
feb_new     = feb_buyers - jan_buyers    # new in February
symmetric   = jan_buyers ^ feb_buyers    # in one but not both

print(f"Retained:    {retained}")     # {'C002', 'C003'}
print(f"Jan churned: {jan_only}")     # {'C001'}
print(f"Feb new:     {feb_new}")      # {'C004'}

For mid-level analysts

Set operations (&, |, -) translate directly to cohort and funnel analysis. "Who signed up in January and also bought in February?" is jan_signups & feb_buyers. "Who churned this month?" is last_month_actives - this_month_actives. These are 10x faster than writing loops for the same logic.


Tuples — Immutable Sequences

Tuples are like lists but cannot be changed after creation. Use them to represent fixed, structured data.

# Creating tuples
coordinates = (28.6139, 77.2090)    # Delhi lat/lon
dimensions  = (1920, 1080)           # screen resolution
rgb_teal    = (13, 148, 136)

# Access and unpack
lat, lon = coordinates               # tuple unpacking
print(lat)    # 28.6139

# Single-element tuple needs a trailing comma
single = (42,)     # tuple
not_a_tuple = (42)  # this is just the integer 42 in parentheses

# Tuples as dictionary keys — lists cannot be dict keys, tuples can
region_data = {
    ("India", "Mumbai"): {"revenue": 45_200, "orders": 190},
    ("India", "Delhi"):  {"revenue": 38_900, "orders": 162},
    ("UK", "London"):    {"revenue": 61_500, "orders": 248},
}
print(region_data[("India", "Mumbai")]["revenue"])    # 45200

# Tuple unpacking in a loop
for (country, city), data in region_data.items():
    print(f"  {country}/{city}: £{data['revenue']:,} ({data['orders']} orders)")

When to use a tuple vs a list: - Tuple: fixed structure (a point, a date range, a key); something that should not change - List: a collection that grows, shrinks, or gets sorted


Choosing the Right Data Structure

Structure Ordered Mutable Unique Use Case
list Yes Yes No Sequence of values, rows of data
dict Yes (3.7+) Yes Keys only Key-value mapping, records
set No Yes Yes Deduplication, membership, cohort ops
tuple Yes No No Fixed record, dict key, return value

Practice Exercises

Warm-up 1. Create a list of 6 monthly revenues. Use slicing to get the last 3 months. Calculate sum, max, and min of the full list without using a loop. 2. Create a dictionary representing a product with keys: product_id, name, category, price, in_stock. Print each key-value pair using .items(). 3. Given customers = ["C1", "C2", "C1", "C3", "C2", "C4", "C1"], find the number of unique customers using a set.

Main 4. You have the orders list from this file. Write a function filter_and_sort(orders, status) that returns orders of the given status, sorted by total descending. 5. Write a dict comprehension that creates {customer_id: total_spend} for completed orders from the orders list. Customers with multiple orders should have their totals summed (hint: use defaultdict or a regular loop, not a comprehension alone). 6. Given jan_buyers and feb_buyers sets, write code to find: (a) buyers who shopped in both months, (b) buyers who churned (only January), (c) all unique buyers across both months.

Stretch 7. Write a function group_by(records, key) that takes a list of dicts and a field name, and returns a dict where keys are unique values of that field and values are lists of matching records. Do not use defaultdict — implement it from scratch. 8. Write a function customer_summary(orders) that takes a list of order dicts and returns a summary dict: {customer_id: {"orders": int, "total_spend": float, "statuses": set}}. The statuses field should contain the unique statuses for that customer's orders.


Interview Questions

[Beginner] What is the difference between a list and a tuple in Python?

Show answer

Both are ordered sequences. The key difference:

  • List ([]): mutable — you can append, remove, and change elements
  • Tuple (()): immutable — once created, it cannot be changed
my_list  = [1, 2, 3]
my_tuple = (1, 2, 3)

my_list[0]  = 99     # works
my_tuple[0] = 99     # TypeError: tuple object does not support item assignment

Use tuples for fixed data (a date range, a geographic coordinate, a record key) where mutability would be a bug. Use lists when you need to grow, shrink, or sort the collection.

Performance note: tuples are slightly faster to create and access than lists, and they use less memory.


[Beginner] How do you check if a key exists in a dictionary?

Show answer
customer = {"id": "C001", "name": "Priya", "segment": "VIP"}

# Method 1: 'in' operator (preferred)
if "email" in customer:
    print(customer["email"])
else:
    print("No email on file")

# Method 2: .get() with a default (most concise)
email = customer.get("email", "not provided")

# Method 3: .get() without default returns None
email = customer.get("email")    # None if key absent
if email is not None:
    print(email)

# WRONG: direct access without checking raises KeyError
print(customer["email"])   # KeyError: 'email'

[Mid-level] Why is checking value in set faster than value in list for large collections?

Show answer

A list stores elements in an array. Checking value in list scans every element one by one: O(n) time. For a list of 10,000 elements, checking membership takes up to 10,000 comparisons.

A set uses a hash table. Checking value in set computes a hash of the value and jumps directly to the right bucket: O(1) average time, regardless of set size.

import time

n = 1_000_000
my_list = list(range(n))
my_set  = set(my_list)

target = n - 1    # worst case for list (it's at the end)

start = time.time()
_ = target in my_list
print(f"List: {(time.time()-start)*1000:.3f}ms")   # ~20ms

start = time.time()
_ = target in my_set
print(f"Set:  {(time.time()-start)*1000:.3f}ms")   # ~0.001ms

Practical rule: if you're doing membership checks in a loop (e.g., "is this customer ID in the VIP list?"), convert the lookup collection to a set first: vip_set = set(vip_ids).


[Mid-level] What is the difference between dict.get(key) and dict[key]?

Show answer
  • dict[key] raises KeyError if the key does not exist
  • dict.get(key) returns None if the key does not exist (or your specified default)
discounts = {"VIP": 0.15, "Regular": 0.05}
segment   = "New"

# WRONG — KeyError
discount = discounts[segment]

# CORRECT — safe
discount = discounts.get(segment, 0)    # 0 for unknown segments

In analytics, always use .get() when: - Mapping codes to labels from a lookup table - Reading optional fields from a JSON/API response - Processing data where some categories may not be in the mapping


[Senior] Explain how Python's list is implemented internally. What is the time complexity of append, insert(0, x), and in operations?

Show answer

A Python list is implemented as a dynamic array (similar to C's realloc).

  • append()O(1) amortised. The array is pre-allocated with extra capacity. Appending fills a slot. When capacity is exhausted, Python allocates a new array (~double the size) and copies all elements. This is rare enough that the average is O(1).

  • insert(0, x)O(n). Inserting at the front requires shifting every existing element one position to the right. On a list with 1 million elements, this copies 1 million elements. Use collections.deque if you need efficient front-insertion.

  • x in listO(n). Scans from start to end. For fast membership, use a set.

from collections import deque
import time

n = 100_000
lst = list(range(n))
dq  = deque(range(n))

start = time.time()
for _ in range(1000): lst.insert(0, -1)
print(f"List insert(0): {time.time()-start:.3f}s")    # ~5s

start = time.time()
for _ in range(1000): dq.appendleft(-1)
print(f"Deque appendleft: {time.time()-start:.3f}s")  # ~0.001s

Previous: 02-functions | Next: 04-file-handling