Data Structures: Intermediate
Wikipedia's internal link-traversal system uses a queue-based breadth-first search to systematically explore related articles, processing millions of page connections by always visiting the closest links before venturing further out. The same BFS queue pattern powers every shortest-path feature in modern software, from Uber's driver routing system to Twitter's trending topic propagation. Stacks and queues are the two structures that make graph traversal possible, and understanding how to implement them in Python is what separates a programmer who can solve algorithmic problems from one who cannot.
Nested Data Structures
Navigate and reshape nested data
Lists of Dicts: Table Data
Access patterns work in two directions. The expression users[0] selects a row by index, returning a dictionary. The expression ["name"] selects a column by key from that dictionary. You can chain these operations: users[0]["name"] gives "Alice" directly. This two-step access pattern is fundamental to working with tabular data in Python.
Dicts with List Values
When you need to group items by a key, use a dictionary where each value is a list. This pattern is common for categorization, grouping database records by a field, and building inverted indexes. The dictionary provides O(1) lookup by group, while the list stores all members of that group. This is more efficient than scanning through a flat list every time you need items from a specific category.
The .items() method yields key-value pairs as tuples, allowing you to iterate over both the group name and its members simultaneously. This pattern enables efficient lookups: finding all engineers is O(1) dictionary access instead of O(n) scanning through every record. For large datasets, this performance difference is significant.
Nested Dicts: Hierarchical
Direct chaining like response["data"]["user"]["profile"] works well when you know the structure exists. However, if any key in the chain is missing, Python raises a KeyError. For uncertain data structures, you need defensive access patterns.
Chaining .get() calls with empty dict defaults {} prevents KeyError exceptions. If any level is missing, the chain returns an empty dict, and subsequent .get() calls safely return their defaults. This pattern is essential when processing API responses that may have optional fields.
Flat to Nested Structures
This pattern of checking for key existence and initializing an empty list is so common that Python provides a cleaner way to write it. The setdefault() method combines the check and initialization into a single operation.
The .setdefault(key, default) method returns the value if the key exists, or sets it to the default and returns that default if the key is missing. This allows you to chain .append() directly, making the grouping operation a single line. This is more Pythonic than the explicit if-check pattern.
> Group items and count total occurrences. Pick the dict method that creates missing keys automatically, and the accessor that returns all the stored lists.
groups = {} for item in ["a", "b", "a", "c", "b"]: groups.(item, []).append(1) print(len(groups)) total = sum( len(v) for v in groups.() ) print(total)
Dict and List Comprehensions
Build lists and dicts in one expression
List Comprehension Basics
A list comprehension has the form [expression for item in iterable]. It creates a new list by evaluating the expression once for each item in the iterable. The expression can be any valid Python expression: a simple variable reference, a calculation, a method call, or even a function application.
Filtering with Conditions
Add an if clause to filter which items are included in the result: [expression for item in iterable if condition]. Only items where the condition evaluates to True are processed and included. The condition is evaluated before the expression, so you can safely access properties that might not exist on filtered-out items.
> You have a list of user dictionaries with name, active status, and role fields. Pick the extraction expression and filter condition to get only active engineers' names.
users = [ {{"name": "Alice", "active": True, "role": "engineer"}}, {{"name": "Bob", "active": False, "role": "analyst"}}, {{"name": "Charlie", "active": True, "role": "engineer"}}, ] result = [ for u in users if ] print(result)
Dictionary Comprehensions
Dictionary comprehensions use curly braces with a key-value pair: {key_expr: value_expr for item in iterable}. They are essential for transforming dictionaries, filtering dictionary entries, building lookups from lists, and inverting key-value relationships.
The zip() function pairs elements from two iterables, creating tuples that you can unpack in the comprehension. Combined with a dict comprehension, it creates dictionaries from parallel lists in one line. This is a very common pattern for data transformation.
Inverting Dictionaries
Inversion only works cleanly when values are unique. If multiple keys share the same value, later entries overwrite earlier ones. When values are not unique, you would need to group keys into lists using the setdefault pattern or defaultdict.
- More lines of code
- Explicit step-by-step logic
- Easier to debug complex logic
- Better for multi-step operations
- Single expression
- Declarative style
- Faster execution
- Better for simple transforms
Nested Comprehensions
Set Comprehensions
Set comprehensions use curly braces like dictionaries, but with single values instead of key-value pairs: {expression for item in iterable}. They automatically deduplicate results, making them perfect for extracting unique values from collections.
Set Operations
Compare datasets with set math
Sets support mathematical operations that are invaluable for data comparison and analysis tasks. Union combines elements from multiple sets. Intersection finds elements common to all sets. Difference finds elements unique to one set. Symmetric difference finds elements in either set but not both. These operations execute in near-constant O(n) time regardless of set size, making them dramatically more efficient than nested loops for comparison tasks.
Union - Combining Sets
Union returns all unique elements from both sets combined. Duplicates are automatically removed since sets only store unique values. Use the | operator or the .union() method. The method form accepts any iterable, not just sets.
Finding Common Elements
Intersection returns only elements present in all sets. Use the & operator or the .intersection() method. This operation is symmetric: A & B equals B & A.
Intersection is invaluable for access control analysis, skill matching, finding common customers between segments, and identifying overlapping records between datasets. The O(1) lookup time of sets makes these operations efficient even for large datasets.
Difference: Unique Elements
Difference returns elements in the first set that are not in the second. Use the - operator or the .difference() method. Unlike union and intersection, difference is not symmetric: A - B is different from B - A.
Symmetric Diff and Subsets
Symmetric difference returns elements in either set but not both, using the ^ operator. Subset and superset checking use <= and >= operators to test containment relationships.
Data Reconciliation
> This code tries to find source records missing from the warehouse, but the set difference operands are reversed. It shows warehouse-only records instead.
Logic error: result shows {6} but should show records in source not in warehouse
Sorting and Filtering
Sort and filter records like SQL
The general pattern for data queries is: filter to select relevant records, sort to order them appropriately, then optionally slice to limit results. This mirrors the SELECT...WHERE...ORDER BY...LIMIT pattern in SQL and appears constantly in data processing code.
Sorting with Custom Keys
The sorted() function accepts a key parameter that specifies how to extract a comparison value from each element. The key function is called once per element, and elements are sorted based on the returned values. This enables sorting complex objects by any attribute or computed property.
The key function is called once per element to extract the sort value. The reverse=True parameter sorts in descending order. Lambda functions are commonly used for simple key extraction, but you can use any callable.
Multi-Level Sorting
To sort by multiple criteria, return a tuple from the key function. Python compares tuples element by element, creating a natural multi-level sort. The first element is the primary sort key, the second is the secondary sort key used to break ties, and so on.
The tuple (r["dept"], -r["years"], r["name"]) sorts first by department alphabetically, then by years in descending order (negation inverts numeric sorting), then by name to break any remaining ties. This gives you fine-grained control over sort order.
> Sort fruit names by their length so the shortest comes first. Pick the function that returns a new sorted list and the key function that measures string length.
words = ["banana", "fig", "apple", "kiwi"] result = (words, key=) print(result[0]) print(result[-1])
Filter, Sort, and Slice
itertools.groupby Grouping
The itertools.groupby() function groups consecutive elements that share the same key value. Important: the data must be sorted by the grouping key first, because groupby only groups consecutive matches. This enables SQL-like GROUP BY operations on sorted data.
The groupby() function yields (key, group_iterator) pairs. The group is an iterator, not a list, so you must convert it with list(group) if you need to iterate it multiple times or access its length. This pattern enables powerful aggregations similar to SQL GROUP BY.
Data Structure Selection
Choose structures by access pattern
Lists vs Sets: Membership
When checking if an item exists in a collection, sets are dramatically faster than lists. Lists scan sequentially from the beginning, making membership testing O(n). Sets use hash tables for near-instant O(1) lookups. For small collections the difference is negligible, but for thousands of items, sets can be hundreds of times faster.
- Order of elements matters
- Duplicates are meaningful
- Index-based access is common
- Sequential iteration needed
- Only unique values matter
- Membership testing is frequent
- Set operations needed
- Order is irrelevant
dict vs list Lookup
When you need to find records by a specific field, dictionaries provide O(1) lookup while lists require O(n) scanning. If you frequently look up records by ID, name, or any other unique key, building a dictionary indexed by that key transforms slow linear searches into instant hash lookups.
Building the dictionary is O(n), but each subsequent lookup is O(1). If you look up records more than once, the preprocessing cost is repaid. For APIs or data processing that repeatedly access records by key, always build lookup dictionaries.
Tuples vs Lists: Immutable
Tuples are immutable sequences. Use them when you need a fixed record that should not be modified, like coordinates, RGB colors, or database rows. Tuples use slightly less memory than lists and, critically, can be used as dictionary keys since they are hashable.
Named tuples from collections.namedtuple provide the benefits of tuples (immutability, hashability) with the readability of named fields. For modern Python, consider @dataclass(frozen=True) which offers similar benefits with additional features like default values and type hints.
Choosing by Operation Type
Memory Considerations
Sets use significantly more memory due to their hash table structure, but this overhead enables O(1) membership testing. The trade-off is worthwhile when lookup speed matters more than memory. Tuples are slightly smaller than lists because they do not allocate extra space for potential growth.
- Profile before optimizing
- Start with the simplest structure
- Convert to specialized types when needed
- Document why you chose each structure
- Prematurely optimize
- Use lists for frequent membership tests
- Forget memory for large datasets
- Assume one structure fits all cases
> You have a list of user IDs and need to quickly check if a given ID exists. Pick the correct type to convert the list into for O(1) lookups, and the correct operator to test membership.
ids = [101, 202, 303, 404] lookup = (ids) print(202 lookup) print(len(lookup))
Common Mistakes
Modifying During Iteration
Mutable Default Arguments
Default argument values are created once when the function is defined, not each time the function is called. If the default is mutable (like a list or dict), modifications persist across function calls, leading to surprising bugs.
Always use None as the default for mutable arguments and create the actual mutable object inside the function body. This is one of Python's most infamous gotchas and a favorite interview question.
Shallow vs Deep Copy
Assignment creates a reference, not a copy. Shallow copy (slice or .copy()) duplicates the outer structure but shares nested objects. Deep copy duplicates everything. Confusing these leads to mysterious bugs where modifying one variable affects another.
Use copy.deepcopy() when you need a completely independent copy of nested structures. This is especially important when working with data you receive from elsewhere and do not want to accidentally modify, or when passing data to functions that might mutate it.
> You are a data engineer at Indeed building a pipeline that parses nested API responses for job postings, transforms salary fields with comprehensions, reconciles active versus expired listing IDs using set operations, and sorts filtered results by relevance score.
.setdefault() and defaultdict simplify building nested structures.get() with empty dict defaults for safe nested access{k: v for k, v in items}| union, & intersection, - difference, ^ symmetric diffkey=lambda x: (x["a"], -x["b"])None as default for mutable function argumentscopy.deepcopy() for fully independent copies of nested dataComplex data manipulation patterns
- Category
- Python
- Difficulty
- intermediate
- Duration
- 48 minutes
- Challenges
- 0 hands-on challenges
Topics covered: Nested Data Structures, Dict and List Comprehensions, Set Operations, Sorting and Filtering, Data Structure Selection
Lesson Sections
- Nested Data Structures (concepts: pyDictNested)
Nested data structures are collections that contain other collections as their elements. This nesting can occur in multiple patterns: a list of dictionaries represents a table of records, similar to rows in a database. A dictionary with list values groups related items by category. A dictionary containing other dictionaries models hierarchical relationships like organizational structures or configuration settings. Understanding these patterns is essential because they mirror the structure of JSO
- Dict and List Comprehensions (concepts: pyListComprehension)
Comprehensions are concise expressions for creating lists, dictionaries, and sets from existing iterables. They combine iteration, transformation, and optional filtering into a single readable line. Beyond being syntactic sugar, comprehensions execute faster than equivalent loops because Python optimizes them internally. They are also considered more Pythonic, expressing intent clearly without the boilerplate of explicit loop construction. Comprehensions shine when you need to transform data fro
- Set Operations (concepts: pySetOperations)
In data engineering, set operations are essential for data reconciliation, deduplication, access control analysis, and finding differences between datasets. Understanding these operations lets you answer questions like "which users have access to both systems?" or "which records exist in the source but not the destination?" with simple, efficient code. Union - Combining Sets Notice that bob and diana appear in both original sets but only once in the union. Sets automatically handle deduplication
- Sorting and Filtering (concepts: pyListSort)
Sorting and filtering are fundamental data operations that are often combined to answer analytical questions. Python provides flexible tools for both: the sorted() function with custom keys enables sophisticated ordering, while comprehensions and the filter() function provide powerful selection capabilities. Mastering the combination of these operations enables you to write complex data queries that rival SQL in expressiveness. Sorting with Custom Keys Multi-Level Sorting Filter, Sort, and Slice
- Data Structure Selection (concepts: pyCollections)
Choosing the right data structure is one of the most important decisions in programming. The wrong choice can make code slow, memory-hungry, or unnecessarily complex. Understanding the strengths and trade-offs of each structure helps you make informed decisions that balance readability, performance, and memory usage for your specific use case. The key insight is that different data structures optimize for different operations. Lists are great for ordered, indexed access. Dictionaries excel at ke