Core Data Structures and ComprehensionsIntroductionData science depends on the ability to represent, organize, transform, and inspect information. Python provides several built-in data structures for these purposes. Strings represent textual data; lists represent ordered collections that may change; tuples represent ordered collections intended to remain stable; sets represent unique, unordered elements; and dictionaries represent associations between keys and values. Choosing an appropriate structure is not merely a matter of syntax. It affects readability, correctness, memory use, computational efficiency, and the reliability of data-processing pipelines. A data scientist who understands mutability, indexing, slicing, equality, identity, hashing, and comprehensions can write programs that are both expressive and robust. This chapter develops these concepts from first principles and applies them to practical data-science tasks such as cleaning text, filtering observations, removing duplicates, constructing feature mappings, and preparing data for analysis. Learning ObjectivesBy the end of this chapter, you should be able to: - manipulate strings, lists, tuples, sets, and dictionaries;
- apply indexing and slicing correctly, including negative indices and steps;
- distinguish mutable from immutable objects;
- explain equality, identity, and aliasing;
- select a suitable data structure for a given problem;
- construct and evaluate list, set, and dictionary comprehensions;
- avoid common errors involving shallow copying, missing dictionary keys, and unintended mutation;
- design readable and maintainable data-structure operations for data-science workflows.
1. Objects, References, and Collection TypesIn Python, a variable name refers to an object rather than containing a value in the same sense as a fixed memory box. For example: scores = [72, 81, 95] The name scores refers to a list object. The list contains references to integer objects. Assignment generally creates another reference to the same object: other_scores = scores
other_scores.append(66)
print(scores) # [72, 81, 95, 66] Both names refer to the same list. This behavior is called aliasing. Python collection types differ along several dimensions: | Type | Ordered | Mutable | Duplicates allowed | Typical use |
|---|
str | Yes | No | Yes | Text and labels | list | Yes | Yes | Yes | Changeable sequences | tuple | Yes | No | Yes | Fixed records or coordinates | set | No guaranteed positional order | Yes | No | Membership and uniqueness | dict | Insertion order preserved | Yes | Keys unique | Key–value lookup |
“Insertion order preserved” for dictionaries means that iteration follows the order in which keys were inserted in modern Python. It does not mean that a dictionary is indexed by integer position. Equality and identityThe operator == tests whether two objects have equivalent contents. The operator is tests whether two names refer to the same object. x = [1, 2]
y = [1, 2]
z = x
print(x == y) # True
print(x is y) # False
print(x is z) # True Use == for value comparison. Use is mainly for identity tests such as value is None. 2. Strings: Text as an Immutable SequenceA string is an immutable sequence of Unicode characters. Immutability means that an existing string cannot be changed in place. Operations that appear to modify a string actually create a new string. text = "Data Science"
text = text.lower()
text = text.replace("science", "analysis")
print(text) # data analysisIndexing and slicing stringsSequence positions begin at zero. Negative indices count from the end. word = "Python"
print(word[0]) # P
print(word[-1]) # n
print(word[1:4]) # yth
print(word[:2]) # Py
print(word[2:]) # thon
print(word[::2]) # Pto
print(word[::-1]) # nohtyP For a sequence of length n, valid positive indices are from 0 through n − 1; valid negative indices are from −n through −1. A slice sequence[start:stop:step] includes start when reachable but excludes stop. The step cannot be zero. Slicing normally returns a new sequence and does not raise an error when a boundary lies outside the sequence, although an integer index outside the valid range raises IndexError. Useful string methodsraw = " Nairobi, Kenya "
clean = raw.strip()
city, country = clean.split(", ")
label = f"{city} ({country})"Important operations include: strip(), lstrip(), and rstrip() for surrounding whitespace;lower() and upper() for case conversion;replace(old, new) for substitution;split(separator) for converting text into a list;join(iterable) for combining strings;startswith() and endswith() for pattern checks;find() for locating a substring;count() for counting non-overlapping occurrences.
join() is called on the separator, not on the list:
words = ["clean", "data", "carefully"]
sentence = " ".join(words) A frequent data-cleaning error is confusing textual replacement with numerical conversion. The string "42" is not the integer 42 until it is explicitly converted using int("42"). 3. Lists: Flexible, Mutable SequencesLists are ordered and mutable. They are appropriate when elements may be added, removed, sorted, or replaced. temperatures = [21.4, 22.1, 20.8]
temperatures.append(23.0)
temperatures[1] = 22.5 The most common list operations are shown below. | Operation | Meaning |
|---|
append(x) | Add one element at the end | extend(iterable) | Add each element from an iterable | insert(i, x) | Insert before position i | remove(x) | Remove the first equal value | pop(i) | Remove and return an item; default is last | clear() | Remove all elements | sort() | Sort the list in place | reverse() | Reverse the list in place | index(x) | Return the first matching position | count(x) | Count equal elements |
append() and extend() behave differently:
items = [1, 2]
items.append([3, 4]) # [1, 2, [3, 4]]
items = [1, 2]
items.extend([3, 4]) # [1, 2, 3, 4] List copying and aliasingThe expression copy = original does not copy a list. Use original.copy() or original[:] for a shallow copy. original = [[1, 2], [3, 4]]
copy_1 = original.copy()
copy_1.append([5, 6]) # original is unaffected at top level
copy_1[0].append(99) # nested inner list is shared A shallow copy duplicates only the outer list. Nested mutable objects remain shared. When independent nested structures are required, copy.deepcopy() may be appropriate, although it should be used deliberately because it can be expensive and may have special behavior for complex objects. Sorting and keyssorted(iterable) returns a new list, whereas list.sort() modifies a list and returns None.
records = [("Amina", 82), ("Jon", 74), ("Li", 91)]
ranked = sorted(records, key=lambda record: record[1], reverse=True)The key function determines the comparison value. In professional code, sorting by a named function or a clear lambda is preferable to relying on the internal arrangement of records. 4. Tuples: Stable Ordered RecordsTuples are immutable ordered sequences. They are useful for fixed collections such as coordinates, dates, database rows, or function return values. location = (−1.2864, 36.8172)
name, score = ("Amina", 88)A one-element tuple requires a trailing comma: not_a_tuple = (5)
a_tuple = (5,) Tuple unpacking improves readability: for student, mark in [("Amina", 88), ("Jon", 76)]:
print(student, mark)Tuples can contain mutable objects. Therefore, tuple immutability means that the tuple’s references cannot be replaced, not that every object reachable through the tuple is immutable. container = ([1, 2], "fixed")
container[0].append(3) # permitted
# container[0] = [9] # TypeError A tuple can be used as a dictionary key only when all its elements are hashable. A tuple containing a list cannot be a key because lists are mutable and unhashable. 5. Sets: Uniqueness and MembershipA set is a mutable collection of unique, hashable elements. Sets do not support positional indexing because they represent membership rather than sequence position. departments = {"sales", "finance", "sales"}
print(departments) # {'sales', 'finance'}; display order may varyAn empty set is created with set(), not {}, because {} creates an empty dictionary. Set operations express common data-processing tasks: a = {1, 2, 3}
b = {3, 4, 5}
print(a | b) # union: {1, 2, 3, 4, 5}
print(a & b) # intersection: {3}
print(a - b) # difference: {1, 2}
print(a ^ b) # symmetric difference: {1, 2, 4, 5}Membership testing with a set is generally efficient because sets are implemented using hash-based lookup. Exact performance depends on implementation and workload, but average-case membership is commonly described as O(1), while scanning a list is O(n), where n is the number of elements. These are asymptotic descriptions, not guarantees of identical running time for every input. Sets are valuable for removing duplicates, checking whether categories are allowed, and comparing groups. They should not be used when order or repeated occurrences are meaningful. 6. Dictionaries: Key–Value Data ModelsA dictionary maps unique, hashable keys to values. It is suitable when data must be retrieved by a meaningful identifier rather than by position. student = {
"id": "ST104",
"name": "Amina",
"score": 88
}
print(student["score"])Dictionary keys must be hashable. Strings, numbers, and suitable tuples can be keys; lists and dictionaries cannot. Access and safe retrievalIndexing with square brackets raises KeyError when the key is absent: score = student["score"] Use get() when a missing key is expected and a fallback is appropriate: email = student.get("email", "not supplied")The methods keys(), values(), and items() provide views of dictionary contents: for field, value in student.items():
print(field, value)To update one or more entries, use assignment or update(): student["score"] = 91
student.update({"status": "pass", "verified": True})Counting with dictionariesA standard data-science pattern is frequency counting: words = ["model", "data", "model", "feature"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1The result is {'model': 2, 'data': 1, 'feature': 1}. For larger applications, collections.Counter offers a specialized and expressive alternative. 7. Indexing, Slicing, and Nested StructuresNested data structures occur frequently in datasets. Consider: sales = [
{"region": "East", "units": [10, 12, 9]},
{"region": "West", "units": [8, 11, 13]}
]The expression sales[1]["units"][2] returns 13. Reading such expressions from left to right is useful: select the second dictionary, retrieve its units value, then select the third unit. For maintainability, unpack complex structures: west_record = sales[1]
west_units = west_record["units"]
latest_west_units = west_units[-1] This style makes assumptions visible and simplifies debugging. 8. Comprehensions: Declarative Collection ConstructionA comprehension constructs a collection from an iterable, optionally transforming and filtering its elements. It is concise, but it should not sacrifice clarity. List comprehensionsThe general pattern is: [expression for item in iterable if condition] For example: squares = [x * x for x in range(1, 6)]
even_squares = [x * x for x in range(1, 11) if x % 2 == 0] The equivalent loop is sometimes clearer for complex logic: squares = []
for x in range(1, 6):
squares.append(x * x)A comprehension is not automatically faster in every context, but it is often efficient and communicates a simple transformation well. Set comprehensionsSet comprehensions create unique results: cities = ["Lagos", "Lima", "Lagos", "Lisbon"]
initials = {city[0] for city in cities}The result contains unique initials. Because sets are unordered, do not depend on the display or iteration order for meaning. Dictionary comprehensionsDictionary comprehensions produce key–value mappings: marks = {"Amina": 82, "Jon": 74, "Li": 91}
passed = {name: mark for name, mark in marks.items() if mark >= 50}If a comprehension produces the same key more than once, the later value replaces the earlier value. This behavior must be considered carefully when transforming data. Nested comprehensionsNested comprehensions can represent nested loops: matrix = [[1, 2], [3, 4], [5, 6]]
flat = [value for row in matrix for value in row] The order corresponds to an outer loop over rows followed by an inner loop over values. Deeply nested comprehensions are usually less readable than explicit loops. Generator expressionsA generator expression uses parentheses and produces values lazily: total = sum(value * value for value in range(1_000_000)) Unlike a list comprehension, it does not construct the entire collection in memory at once. This can be beneficial for large streams, although a generator can generally be consumed only once. 9. Selecting a Suitable StructureStructure selection should follow the data’s conceptual requirements. | Requirement | Suitable structure | Reason |
|---|
| Ordered, changeable observations | List | Supports indexing and mutation | | Fixed record with positional fields | Tuple | Communicates stability | | Unique categories or fast membership | Set | Removes duplicates and supports set algebra | | Lookup by identifier | Dictionary | Expresses key–value relationships | | Textual sequence | String | Specialized immutable sequence |
Consider the following questions: - Does order matter?
- Are duplicates meaningful?
- Must the collection change after creation?
- Will elements be retrieved by position or by name?
- Must membership testing be frequent?
- Are the elements hashable?
- Will other programmers understand the representation?
A list of dictionaries is often useful for small tabular datasets, but analytical workloads commonly benefit from specialized libraries such as pandas, whose Series and DataFrame structures provide columnar operations, missing-value handling, alignment, and vectorization. Python built-ins remain essential because they underpin and complement such tools. 10. Worked Example: Cleaning and Summarizing Survey ResponsesSuppose a survey stores responses as strings: responses = [
" Python ", "python", "R", "SQL", "Python", " sql ", "Java"
]The objective is to normalize the responses, identify unique technologies, count occurrences, and retain only technologies appearing at least twice. First normalize whitespace and case: normalised = [response.strip().lower() for response in responses] The result is: ['python', 'python', 'r', 'sql', 'python', 'sql', 'java'] Create the unique set: unique_technologies = set(normalised) This produces the members python, r, sql, and java; the display order is not analytically meaningful. Count frequencies: frequencies = {}
for technology in normalised:
frequencies[technology] = frequencies.get(technology, 0) + 1The resulting dictionary is: {'python': 3, 'r': 1, 'sql': 2, 'java': 1}Filter frequent technologies: frequent = {
technology: count
for technology, count in frequencies.items()
if count >= 2
}The result is {'python': 3, 'sql': 2}. This example illustrates a pipeline in which a list preserves observations, a set represents unique categories, and a dictionary stores aggregated counts. 11. Applied Case: Preparing Product Events for AnalysisBackgroundAn online retailer records product events as dictionaries. Each event contains a customer identifier, a product code, and an event type. events = [
{"customer": "C1", "product": "P7", "event": "view"},
{"customer": "C1", "product": "P7", "event": "cart"},
{"customer": "C2", "product": "P3", "event": "view"},
{"customer": "C1", "product": "P7", "event": "view"},
]ProblemThe analyst needs to determine which customers interacted with product P7, remove duplicate customer identifiers, and count event types. Analysis and approachA list is appropriate for the raw event sequence because each observation is retained. A set is appropriate for the unique customers associated with P7: p7_events = [event for event in events if event["product"] == "P7"]
customers = {event["customer"] for event in p7_events}The result is {'C1'}. The repeated view event does not create a duplicate customer. A dictionary can count event types: event_counts = {}
for event in p7_events:
kind = event["event"]
event_counts[kind] = event_counts.get(kind, 0) + 1The result is {'view': 2, 'cart': 1}. OutcomeThe retailer can report that one unique customer interacted with P7, generating two views and one cart event. The representation also supports later extension, such as grouping by customer or product. Learning points- Raw records and derived summaries may require different structures.
- Sets remove duplicate identities but should not replace event records.
- Dictionaries make grouped counts explicit.
- A comprehension is appropriate for a short, transparent filter; a conventional loop may be preferable when validation, logging, or multiple operations are required.
12. Common Errors and Best PracticesAccidental mutationAvoid modifying a collection while iterating over it. Create a filtered result instead: values = [1, 2, 3, 4]
values = [value for value in values if value % 2 == 0] Mutable default argumentsDo not use a mutable object as a default parameter when the intention is to create a fresh object for each call: def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return itemsDictionary accessUse get() when absence is normal; use square brackets when absence indicates an error that should be detected. Readability and validationComprehensions should normally contain one clear transformation and, at most, simple filtering. Validate input before conversion, document assumptions about missing values, and use descriptive names. For sensitive data, avoid exposing personal identifiers in debugging output or logs. Ethics and governanceData structures do not eliminate ethical responsibilities. When processing customer, student, health, or employee data: - collect and retain only necessary information;
- protect identifiers and access permissions;
- avoid using dictionary keys or set membership as a substitute for authorization controls;
- preserve provenance when transforming records;
- document cleaning rules because normalization can alter interpretation;
- ensure that deduplication does not incorrectly merge distinct individuals.
For example, lowercasing names may be suitable for matching in one context but may erase meaningful distinctions in another. Technical convenience must not override domain knowledge or privacy requirements. 13. Contemporary Practice and ExtensionModern data-science systems often move from Python’s built-in structures to typed models, NumPy arrays, pandas tables, databases, and distributed dataframes as scale increases. Nevertheless, built-ins remain the language-level foundation for configuration, parsing, API responses, feature engineering, and test fixtures. Type hints can communicate intended structure: from typing import TypedDict
class Event(TypedDict):
customer: str
product: str
event: strType hints do not automatically validate runtime data, but they improve documentation, editor support, and static analysis. In production pipelines, combine clear types with explicit validation and tests. Chapter SummaryStrings are immutable ordered sequences used for textual data. Lists are mutable ordered collections suitable for observations that may change. Tuples are immutable ordered records, while sets represent unique hashable elements and support union, intersection, and difference. Dictionaries map unique hashable keys to values and are especially useful for lookup, grouping, and counting. Indexing selects one element and slicing selects a range using start:stop:step; slices exclude the stop boundary. Assignment creates references, so aliasing and shallow copies must be understood when mutable objects are involved. Equality compares contents, whereas identity compares object references. List, set, and dictionary comprehensions express concise transformations and filters. Generator expressions provide lazy evaluation for potentially large sequences. The appropriate structure depends on order, duplication, mutability, access pattern, and membership requirements. In professional data science, structure choice must also consider readability, scalability, privacy, provenance, and the risk of incorrect deduplication or normalization. Key TermsObject: A value managed by Python that may be referenced by one or more names. Reference: A relationship in which a name refers to an object. Aliasing: The condition in which multiple names refer to the same object. Mutable: Capable of being changed after creation. Immutable: Not changeable in place after creation. Indexing: Selecting an individual element by position or key. Slicing: Selecting a subsequence using start, stop, and step boundaries. String: An immutable Unicode sequence of characters. List: A mutable ordered collection. Tuple: An immutable ordered collection. Set: A collection of unique hashable elements. Dictionary: A mutable mapping of unique keys to values. Hashable: Suitable for use in hash-based structures such as sets and dictionary keys. Comprehension: A compact expression for constructing a collection. List comprehension: A comprehension that creates a list. Set comprehension: A comprehension that creates a set of unique results. Dictionary comprehension: A comprehension that creates key–value pairs. Generator expression: A lazy expression that produces values progressively. Shallow copy: A copy of an outer collection whose nested objects may remain shared. Normalization: Transforming data into a consistent representation, such as standard case and whitespace. Review QuestionsShort Answer Questions- What is the difference between equality and identity in Python?
- Why does
append() differ from extend() when used with a list? - Why can a set not be indexed by position?
- What is the purpose of the default value in
dictionary.get(key, default)? - Why is a trailing comma required to create a one-element tuple?
Long Answer Questions- Compare lists, tuples, sets, and dictionaries in terms of ordering, mutability, duplication, and typical data-science use.
- Explain Python slicing, including start, stop, step, negative indices, and boundary behavior.
- Discuss aliasing and shallow copying, using nested lists to illustrate the difference.
- Explain list, set, and dictionary comprehensions and provide an appropriate use case for each.
- Evaluate the factors that should guide data-structure selection in a production data-processing pipeline.
Analytical / Application Questions- Given a list of inconsistent product categories containing whitespace, mixed case, and duplicates, design a pipeline that normalizes the values, identifies unique categories, and counts their frequencies. Justify the structure used at each stage.
- A team uses a list to test membership among several million permitted identifiers. Analyze the likely performance and design implications, and recommend a more suitable structure where appropriate.
- A customer dataset contains repeated events and similar names. Explain how careless use of sets, lowercasing, or dictionary keys could produce incorrect analytical results, and propose safeguards.
Multiple Choice Questions with AnswersQuestion 1Which Python structure is most suitable for storing unique department names when order is not important? A. List B. Tuple C. Set D. String Correct Answer: C. Set Question 2What does the slice text[1:5] include? A. Positions 1 through 5 B. Positions 1 through 4 C. Positions 0 through 5 D. Only position 5 Correct Answer: B. Positions 1 through 4 Question 3What is the result of a = [1, 2]; b = a; b.append(3); print(a)? A. [1, 2] B. [3] C. [1, 2, 3] D. An empty list Correct Answer: C. [1, 2, 3] Question 4Which operation safely supplies a fallback when a dictionary key may be absent? A. d.key() B. d.get(key, fallback) C. d.index(key) D. d.value(key) Correct Answer: B. d.get(key, fallback) Question 5What collection is produced by {x * 2 for x in [1, 2, 2]}? A. A list containing 2, 4, 4 B. A tuple containing 2, 4 C. A set containing 2, 4 D. A dictionary mapping 1 to 2 Correct Answer: C. A set containing 2, 4 Question 6Which statement about sorted(values) is correct? A. It always modifies values in place B. It returns a new sorted list C. It returns a set D. It can only sort strings Correct Answer: B. It returns a new sorted list Question 7Which object can serve as a dictionary key? A. A list B. A dictionary C. A mutable set D. A tuple containing only hashable elements Correct Answer: D. A tuple containing only hashable elements Question 8What is the main advantage of a generator expression over a list comprehension for a large one-pass calculation? A. It guarantees sorted output B. It stores all results permanently C. It can produce values lazily D. It removes all duplicates Correct Answer: C. It can produce values lazily Question 9What does " ".join(["data", "science"]) return? A. data science B. datascience C. ['data', 'science'] D. data, science Correct Answer: A. data science Question 10In the frequency-counting pattern counts[word] = counts.get(word, 0) + 1, what value is used when word is not already a key? A. None B. 1 C. 0 D. The length of counts Correct Answer: C. 0
|