Data Types II
Lists, tuples, sets, dictionaries, and ownership
Lists
A list is an ordered, mutable sequence. It supports duplicates, indexing, slicing, and membership checks:
regions = ["mumbai", "singapore", "frankfurt"]
print(regions[0], regions[-1]) # mumbai frankfurt
print(regions[:2]) # ['mumbai', 'singapore']
print("singapore" in regions, len(regions)) # True 3
regions[1] = "tokyo"
print(regions) # ['mumbai', 'tokyo', 'frankfurt']A slice makes a new outer list containing the selected references. It can be empty or shorter than requested; indexing a missing position raises IndexError.
| Operation | Behavior |
|---|---|
items.append(value) | Add one value. Another list is added as one element. |
items.extend(other) | Add the elements of another iterable, such as a list. |
items.insert(index, value) | Insert before a position. |
items.pop() | Remove and return the last element; pop(index) selects a position. |
items.remove(value) | Remove the first equal value; raise ValueError if absent. |
items.copy() | Copy the outer list, retaining its element references. |
items.sort() | Sort in place; return None. |
sorted(items) | Return a new sorted list. |
Routing attempts are order-sensitive; duplicates can represent retries:
attempts = ["old-region", "singapore"]
attempts.insert(0, "mumbai")
attempts.extend(["tokyo", "singapore"])
attempts.remove("old-region")
last_attempt = attempts.pop()
print(attempts) # ['mumbai', 'singapore', 'tokyo']
print(last_attempt) # singaporeSorting would change that routing order. Popping an empty list raises IndexError.
Tuples
A tuple is an ordered sequence whose element references cannot be replaced. Use it for a small, fixed record:
target = ("payments", "staging")
service, environment = target # Unpack the two values
print(service, environment) # payments staging
print(target == ("payments", "staging")) # True
print(("payments",)) # ('payments',): one-element tupleThe comma creates the one-element tuple; ("payments") is just a string. Use a list when elements need to be added, removed, or reordered. A dictionary or named record suits data with many fields.
Sets
Sets contain unique, hashable values and support membership checks, union (|), intersection (&), and difference (-). They do not promise a meaningful iteration order.
required = {"deploy", "read-logs"}
available = {"deploy", "read-metrics"}
print(sorted(required & available)) # ['deploy']
print(sorted(required - available)) # ['read-logs']
print(sorted(required | available)) # ['deploy', 'read-logs', 'read-metrics']Intersection finds required permissions the caller has; difference finds missing permissions. Union combines both groups and would grant permissions the caller may lack. sorted() makes the display predictable.
set()creates an empty set;{}creates a dictionary.add(value)adds a member; duplicates have no effect.remove(value)raisesKeyErrorif absent;discard(value)is harmless when absent.copy()creates an independent outer set.
Hashable values have a stable hash for lookup. Strings and integers work; ordinary lists and dictionaries do not. A tuple is hashable only when all its elements are hashable. Custom equality and hashing are covered later.
Dictionaries
A dictionary maps unique, hashable keys to values. Values can include other containers:
settings = {"workers": 2, "enabled": True, "fallback": None}
settings["workers"] = 4
settings.update({"enabled": False, "region": "mumbai"})
print(settings["workers"], settings["enabled"]) # 4 False
print(settings.get("fallback", "automatic")) # None
print(settings.get("missing", "automatic")) # automatic
print("fallback" in settings, "missing" in settings) # True False
del settings["region"]
print(list(settings.keys())) # ['workers', 'enabled', 'fallback']update() replaces matching entries and adds new ones. get(key, default) uses the default only for a missing key; a stored None, False, or 0 remains a value. Use key in mapping to distinguish missing from present.
mapping[key] raises KeyError if absent. del mapping[key] deletes an existing entry; mapping.pop(key, default) removes and returns a value, using the default if absent.
keys(), values(), and items() return live views. Convert a view to a list for a snapshot. Dictionaries preserve insertion order: updating a key keeps its position; deleting and reinserting moves it to the end.
Object references and copies
Assignment creates another reference, not a copy. A shallow copy creates a new outer container while sharing its elements:
defaults = {"workers": 2, "labels": ["api"]}
alias = defaults
candidate = defaults.copy()
candidate["workers"] = 4
candidate["labels"].append("canary")
print(alias is defaults, candidate is defaults) # True False
print(defaults) # {'workers': 2, 'labels': ['api', 'canary']}
print(candidate) # {'workers': 4, 'labels': ['api', 'canary']}Replacing candidate["workers"] affects only the copied dictionary. Appending to the shared label list affects both. Copy each mutable level the caller needs to own; immutable strings can remain shared.
Example: an independent deployment preview
Copy the label list before editing it, and use set difference to report missing permissions:
defaults = {"workers": 2, "labels": ["api"]}
candidate = defaults.copy()
candidate["labels"] = defaults["labels"].copy()
candidate["labels"].append("canary")
candidate["workers"] = 4
preview = {"target": ("payments", "staging"), "settings": candidate}
required = {"deploy", "read-logs"}
available = {"deploy"}
print(preview["target"]) # ('payments', 'staging')
print(preview["settings"]) # {'workers': 4, 'labels': ['api', 'canary']}
print(sorted(required - available)) # ['read-logs']
print(defaults) # {'workers': 2, 'labels': ['api']}The target is a fixed pair, settings use named fields, labels remain editable, and permission checks use sets. The defaults remain independent of the preview's edits.
Gotchas
Repeated references and nested mutability
Multiplication repeats references; a tuple can still refer to mutable data:
groups = [[]] * 3
groups[0].append("canary")
print(groups) # [['canary'], ['canary'], ['canary']]
record = ("payments", ["api"])
record[1].append("canary")
print(record) # ('payments', ['api', 'canary'])The three group entries share one list. The tuple's references stay fixed, but the list it refers to can change. For independent groups, create separate lists.
Mutation methods return None
Methods such as sort(), append(), and dict.update() mutate their receiver and return None:
labels = ["worker", "api"]
result = labels.sort()
print(labels, result) # ['api', 'worker'] None
labels = ["worker", "api"]
ordered = sorted(labels)
print(labels, ordered) # ['worker', 'api'] ['api', 'worker']Use sort() to change an existing list's order; use sorted() to retain the original. Methods that return a removed value, such as pop(), have a different contract.
Operation costs
In CPython, lists are resizable arrays of object references:
- Index access is constant-time; end append is amortized constant-time.
- Membership can scan the whole list; front insertion/removal shifts references. Both are linear in list length.
- Dictionary and set lookup is usually average constant-time, with memory overhead and worst-case exceptions.
A list of integers also references Python integer objects. The standard library's array type offers compact, homogeneous numeric storage. Choose a container for the operations and representation required; lists remain useful for ordered, editable sequences.
Put it into practice
Implement the exercise in a full-screen workspace, then run its tests.