Collection Data Types
A collection is a single object that groups multiple values (like basic types) together and so they can be stored in one variable together and worked with as a unit.
| Collection Type | Example | Access values by | Mutable | Use it for |
|---|---|---|---|---|
list |
|
position # |
|
|
dictionary "dict" |
|
Name of a key |
|
|
tuple |
|
position # |
|
|
set |
|
Membership (in) |
|
Check what type a variable is
type() shows the data type
isinstance() checks whether a value is that type.
weights = [5, 3, 6]
type(weights) # <class 'list'>
isinstance(weights, list) # True
isinstance(weights, dict) # False
Lists
Create a list
-
A list stores multiple items, in order, inside a single variable. The items can be any type.
-
The index is the numbered position of an item. The index of the first item is 01, next is 1, and so on.
species = ["burmese", "rock", "ball", "blood"]
block-beta
block:diagram
columns 5
lblValue["Value"] v0[""burmese""] v1[""rock""] v2[""ball""] v3[""blood""]
lblIndex["Index"] i0["0"] i1["1"] i2["2"] i3["3"]
lblNeg["Negative index"] n0["-4"] n1["-3"] n2["-2"] n3["-1"]
end
classDef label fill:none,stroke:none,color:#8A8370
classDef panel fill:#23221E,stroke:#35342E,stroke-width:1px
class i0,i1,i2,i3,n0,n1,n2,n3,lblValue,lblIndex,lblNeg label
class diagram panel
FIG: list values with their positive and negative indexes
-
The negative index tells you how far from the end it is. It starts counting down from the end instead, starting at
-1for the last item, -2 for the second-to-last, and so on. -
Each item can be referenced by its positive or negative index.
Access and update items
-
Index with
list[index]to return the item at that index (position number) of the list.To update the item at that index, set it equal to something else
list[index] = new_item. This works because a list is mutable — updating an item changes it in place instead of building a new one, the same way an object's attributes can be changed after it's created.Run the below example, and change the indexes to see how they work:
species = ["burmese", "rock", "ball", "blood"] print(species) print(species[0]) # "burmese" print(species[1]) # "rock" print(species[-1]) # "blood" species[1] = "carpet" # update item at index 1 print(species) # ["burmese", "carpet", "ball", "blood"] -
Access a range of multiple items at once:
-
Slice with
list[start:end]to return a new list containing items from thestartindex up to (but not including) theendindex.species[1:3] # ["rock", "ball"], starts at index 1, stops at (doesn't include) index 3 -
Step with
list[start:end:step]to return a new list that can skip items instead of taking every one —start/endare optional so if you leave them off the step is applied to the whole list. A step of-1walks backward, which is the standard trick for reversing a list. You can also add a start and end range just like a slice.species[::2] # ["burmese", "ball"] — every 2nd item species[::-1] # ["blood", "ball", "rock", "burmese"] — reversed
Assigning to a range
Setting a slice or step
=equal to a list will replaces that whole range at once with the new list.A plain slice accepts a replacement of any length — it doesn't need to match the range being replaced.
species[1:3] = ["carpet", "rock"] # ["burmese", "carpet", "rock", "blood"]A step, on the other hand, requires the replacement to supply exactly as many items as the step selected, or Python raises a
ValueError.species[::2] = ["carpet", "anaconda"] # ["carpet", "rock", "anaconda", "blood"] -
Loop through a list
-
Lists make it simple to loop directly over the items. The loop runs once for every item in the list, and on each pass the new loop variable, (i.e.
specie) is set to the next item in the list.for specie in species: print(specie) # specie is "burmese", then "rock", then "ball", then "blood" — one item per pass -
If you also want the index of the item alongside the item itself,
enumerate()hands back both together.for index, specie in enumerate(species): print(index, specie) # 0 "burmese", then 1 "rock", then 2 "ball", then 3 "blood"
Boolean expressions
-
in,not inchecks whether a value exists or is missing in the list."burmese" in species # True "anaconda" not in species # True -
==,!=checks whether a specific item is equal or not equal to something.species[0] == "burmese" # True species[0] != "blood" # TrueWhen comparing whole lists, it will say if the two lists have the same contents, in the same order.
other_species = ["burmese", "rock", "ball", "blood"] species == other_species # True species != ["ball", "burmese"] # True -
is,is notchecks whether two variables point to the exact same list object, not just an equal one.same_species = species # another name for the same list copy_of_species = species[:] # a separate list, equal contents species is same_species # True, same list species is copy_of_species # False, different list with equal contents -
is list emptytruthiness (boolean value of the whole list)-
Truthy: a list with contents
-
Falsy: an empty list
[]
if species: # runs if the list has items print("found some") while species: # loops until the list is empty species.pop() -
List operations
Inspect
-
len()returns how many items are in a list.len(species) # 4 -
index()finds the position of the first match.species.index("burmese") # 0 -
count()counts how many times a value appears.species.count("ball") # 1
Add item
-
append()adds one item to the end of the list.species.append("carpet") # ["burmese", "rock", "ball", "blood", "carpet"] -
insert()adds an item at a specific index, without overwriting what's already there.species.insert(1, "carpet") # ["burmese", "carpet", "rock", "ball", "blood"] -
extend()adds every item from another iterable.species.extend(["carpet", "central african rock"]) # ["burmese", "rock", "ball", "blood", "carpet", "central african rock"]
Remove item
-
remove()deletes the first item that matches a given value. If there are duplicate items, it only removes the first one.species.remove("rock") # ["burmese", "ball", "blood"] -
pop()deletes an item by index and returns it — with no index, it removes the last item.species.pop() # "blood" (removed and returned last item) species.pop(0) # "burmese" (removed and returned item at index 0) -
delremoves an item by index, or can delete the entire list.del species[0] # ["rock", "ball", "blood"] del species # deletes the whole list — species no longer exists -
clear()empties the list but keeps the (now empty) list around.species.clear() # []
Sort
-
sort()sorts the list in place, alphabetically (or ascending, for numbers) by default. Passreverse=Trueto sort in the opposite order, or akeyfunction to control what each item is sorted by.species.sort() # ["ball", "blood", "burmese", "rock"] species.sort(reverse=True) # ["rock", "burmese", "blood", "ball"] species.sort(key=len) # ["rock", "ball", "blood", "burmese"] -
sorted()does the same job assort(), but returns a new list, leaving the original untouched.sorted(species) # same as sort() above, but returns a new list -
reverse()flips the current order in place — different fromsort(reverse=True), since it doesn't actually sort, just reverses.species.reverse() # ["blood", "ball", "rock", "burmese"]
Arithmetic
-
min()finds the smallest item.min(length_ft) # 3.5 -
max()finds the largest item.max(length_ft) # 12 -
sum()adds every item together.sum(length_ft) # 25
Create
-
list()builds the same list from any iterable, if you'd rather not use literal brackets.list(("burmese", "rock", "ball", "blood")) # ["burmese", "rock", "ball", "blood"] -
+joins two lists into a new one.species + ["carpet", "boa"] # ["burmese", "rock", "ball", "blood", "carpet", "boa"] -
copy()makes a real, independent copy of the list — unlikenew_list = old_list, which just points a second name at the same list, so a change through either name shows up in both.same_list = species # same_list and species are the same list — changing one changes both backup = species.copy() # backup is a separate, independent list
List comprehension
-
[expr for item in iterable]builds a new list from an existing iterable in a single line. The expression part can transform each item, not just filter it.[s for s in species if len(s) > 4] # ["burmese", "blood"] [s.title() for s in species] # ["Burmese", "Rock", "Ball", "Blood"]Swapping the brackets for parentheses turns this into a generator expression instead — same syntax, but it produces items one at a time rather than building the whole list up front. Use a list comprehension when the result needs indexing,
len(), or looping over more than once; use a generator expression when it's only read once, or the full result would be too large to hold in memory as a list.
Going further
In-place list methods return None
append(), insert(), extend(), sort(), reverse(), and remove() all change the list directly and return None — not the changed list. Reassigning the variable to one of their results replaces the list itself with None, and the next call on it raises AttributeError: 'NoneType' object has no attribute '...'.
species = species.append("carpet") # species is now None, not the updated list
species.sort() # AttributeError: 'NoneType' object has no attribute 'sort'
Call the method on its own line instead — the list was already changed in place, nothing to reassign.
species.append("carpet") # correct — no assignment needed
Practice with lists
Each box below is fully editable — write your answer, then click Run.
1. Access & check membership. Print the second item in the list (index 1), then check whether "rock" is in it.
species = ["indian", "carpet", "rock", "angolan", "blood"]
# your code here
2. Change & add. Change the first item to "burmese", then add "bornean" to the end.
species = ["indian", "carpet", "rock", "angolan", "blood"]
# your code here
print(species)
3. Remove. Remove "carpet" from the list, then pop the last item and print what was removed.
species = ["indian", "carpet", "rock", "angolan", "blood"]
# your code here
4. List comprehension. Build a list of just the names with more than 5 letters.
species = ["indian", "carpet", "rock", "angolan", "blood"]
# your code here
print(long_names)
5. Sort. Sort the list in reverse alphabetical order.
species = ["indian", "carpet", "rock", "angolan", "blood"]
# your code here
print(species)
Show solutions
# 1. Access & check membership
species = ["indian", "carpet", "rock", "angolan", "blood"]
print(species[1])
print("rock" in species)
# 2. Change & add
species = ["indian", "carpet", "rock", "angolan", "blood"]
species[0] = "burmese"
species.append("bornean")
print(species)
# 3. Remove
species = ["indian", "carpet", "rock", "angolan", "blood"]
species.remove("carpet")
last = species.pop()
print(last)
# 4. List comprehension
species = ["indian", "carpet", "rock", "angolan", "blood"]
long_names = [s for s in species if len(s) > 5]
print(long_names)
# 5. Sort
species = ["indian", "carpet", "rock", "angolan", "blood"]
species.sort(reverse=True)
print(species)
Extending lists with collections.deque
A list can already add or remove items from the end cheaply, but doing the same at the
front — species.insert(0, item) or species.pop(0) — means Python has to shift every
other item over. The collections library's
deque adds fast appendleft()/popleft() methods for
exactly that case. Switch to it when items are being added or removed from both ends
often, like a queue of items processed in the order they arrive — not for a list that's
mostly read or only changed at the end, where a plain list is simpler and already fast.
See the collections library page for the rest of deque's
methods (rotate(), maxlen=, and more) and for the other list-adjacent tools it adds.
For efficiency, use append()/pop() instead of insert(0, x)/pop(0)
| Time | Space | |
|---|---|---|
append() / pop() |
O(1) | O(1) |
insert(0, x) / pop(0) |
O(n) | O(1) |
append() and pop() (from the end) run in constant time — one step no matter how long the list already is. insert(0, item) and pop(0) run in linear time, since Python has to shift every remaining item over.
See Efficiency for why this distinction matters.
For efficiency, sorted() copies the list; sort() doesn't
| Time | Space | |
|---|---|---|
sort() |
O(n log n) | O(1) |
sorted() |
O(n log n) | O(n) |
sort() rearranges the list in place, while sorted() builds and returns an entirely new one, so both copies sit in memory at once until the original is no longer needed.
Reach for sort() when the original order doesn't need to survive; sorted() when it does.
See Efficiency for why this distinction matters.
Dictionaries
Create a dictionary
- A dictionary stores data as key-value pairs, inside a single variable. Values are looked up by key, not by a numbered position like a list's index — a dict does remember the order keys were added in, but that order isn't how you access anything.
snake = {
"species": "ball",
"length_ft": 5,
"venomous": False
}
%%{init: {"flowchart": {"nodeSpacing": 15}}}%%
flowchart LR
subgraph snake["snake"]
direction LR
lblKey["`*Key*`"] ~~~ lblVal["`*Value*`"]
key1["species"] --> val1["'ball'"]
key2["length_ft"] --> val2["5"]
key3["venomous"] --> val3["False"]
end
style key1 stroke:#3f6b52,stroke-width:2px
style key2 stroke:#3f6b52,stroke-width:2px
style key3 stroke:#3f6b52,stroke-width:2px
style lblKey fill:none,stroke:none,color:#8A8370
style lblVal fill:none,stroke:none,color:#8A8370
FIG: a dict's key-value pairs
-
Each key points to exactly one value.
-
A key's type can be a string, int, float, or tuple
-
No duplicate keys — assigning a value to an existing key overwrites its value.
-
-
A value can be any type.
Access a value
-
dict[key]accesses a value by key, in square brackets. This raisesKeyErrorif the key is missing — use it when a missing key means something's wrong and should surface as an error.snake["species"] # "ball" -
get()does the same thing, but returnsNoneif the key is not in the dict, instead of raising an error. Use it when a missing key is an expected possibility. You can provide an optional default value to fall back on that will be returned if the key is not in the dict.snake.get("species") # "ball" snake.get("weight_lbs", 0) # 0 — key is missing, so the default is returned instead of None
For efficiency, use .get() instead of checking in first
| Time | Space | |
|---|---|---|
if key in snake: snake[key] |
O(1) (two lookups) | — |
snake.get(key) |
O(1) (one lookup) | — |
if key in snake: value = snake[key] does two hash lookups — one to check membership, one to fetch the value. snake.get(key) does the same job in one. Both are O(1), so this isn't a Big O difference, just avoided repeated work — worth reaching for out of habit once it's familiar, not worth restructuring existing code to chase.
See Efficiency for why this distinction matters.
Loop through a dictionary
-
Looping directly over a dictionary gives you its keys, one at a time — the loop runs once for every key in the dictionary, and on each pass the loop variable, (i.e.
key) is set to the next key.for key in snake: print(key) # species length_ft venomous -
Loop over
.values()to get just the values instead.for value in snake.values(): print(value) # ball 5 False -
Loop over
.items()to get both the key and the value together.for key, value in snake.items(): print(key, value) # species ball length_ft 5 venomous False
Boolean expressions
-
inchecks whether a key exists at all."species" in snake # True -
not inchecks whether a key is missing. Or compare a specific value directly, likesnake["length_ft"] > 2."habitat" not in snake # True -
==checks whether two dictionaries have the same keys and values, even if they're different objects.!=checks whether they differ.other_snake = {"species": "ball", "length_ft": 5, "venomous": False} snake == other_snake # True -
ischecks whether two variables point to the exact same dictionary object, not just an equal one — use==to compare contents.same_snake = snake # another name for the same dictionary copy_of_snake = snake.copy() # a separate dictionary, equal contents snake is same_snake # True, same dictionary snake is copy_of_snake # False, different dictionary with equal contents -
boolean expression:
-
Truthy: a dictionary with at least one key
-
Falsy: an empty dictionary
{}
if snake: # runs if dictionary is not empty print("found a record") while snake: # loops until the dictionary is empty snake.popitem() -
Dictionary operations
Inspect
-
len()returns how many key-value pairs are in a dictionary.len(snake) # 3
Update
-
dict[key] = valuesets a key's value — changes it if the key already exists, adds it if not.snake["length_ft"] = 6 # {'species': 'ball', 'length_ft': 6, 'venomous': False} snake["origin"] = "west africa" # {'species': 'ball', 'length_ft': 5, 'venomous': False, 'origin': 'west africa'} -
update()does the same for multiple keys at once — changes any that already exist, and adds any that don't.snake.update({"venomous": False, "docile": True}) # {'species': 'ball', 'length_ft': 5, 'venomous': False, 'docile': True}
Remove
-
pop()removes a key and returns its value.snake.pop("venomous") # False (removed and returned) -
popitem()removes and returns the last inserted key-value pair, as a tuple.snake.popitem() # ('venomous', False) — removes the last inserted pair -
delremoves a key-value pair by key.del snake["species"] # {'length_ft': 5, 'venomous': False} -
clear()empties the dictionary but keeps the (now empty) dictionary around.snake.clear() # {}
Create
-
dict()builds the same dictionary using keyword arguments, if you'd rather not use literal braces.dict(species="ball", length_ft=5, venomous=False) # {'species': 'ball', 'length_ft': 5, 'venomous': False} -
copy()makes a real, independent copy of the dictionary — unlikenew_dict = old_dict, which just points a second name at the same dictionary, so a change to either would effect both.same_dict = snake # same_dict and snake are the same dictionary — mutating one mutates both backup = snake.copy() # backup is a separate, independent dictionary
Nested dictionaries
A dictionary's values can be other dictionaries. Useful for grouping related records under one variable, like a whole collection of snakes keyed by species.
Chain operations one after the other to reach a value nested inside an inner dictionary.
snakes = {
"ball": snake,
"burmese": {
"length_ft": 16,
"venomous": False
}
}
snakes["burmese"]["length_ft"] # 16
For efficiency, use a dict instead of a list to look up by key
| Time | Space | |
|---|---|---|
Dict dict[key] / .get() |
O(1) | — |
List of (key, value) tuples, searched by hand |
O(n) | — |
Looking up a key with dict[key] or .get() is O(1) — Python computes where to look directly, the same cost regardless of how many keys the dict holds. Storing the same data as a list of (key, value) tuples instead and searching for a match by hand is O(n) — worst case, checking every pair before finding it or coming up empty. That's the main reason to reach for a dict instead of a list when data needs to be looked up by a key.
See Efficiency for why this distinction matters.
Going further
Practice with dictionaries
Each box below is fully editable — write your answer, then click Run.
1. Add & change. Add a "docile" key set to True, then change "length_ft" to 16.
snake = {"species": "burmese", "length_ft": 12, "venomous": False}
# your code here
print(snake)
2. Remove. Remove the "venomous" key from the dictionary.
snake = {"species": "burmese", "length_ft": 16, "venomous": False}
# your code here
print(snake)
3. Loop and collect. Build a list of just the dictionary's values, using a loop (not list(snake.values())).
snake = {"species": "burmese", "length_ft": 16, "venomous": False}
values = []
# your code here
print(values)
4. Nested access. Given the dictionary below, print the ball python's length.
snakes = {
"burmese": {"length_ft": 16, "venomous": False},
"ball": {"length_ft": 5, "venomous": False},
}
# your code here
Show solutions
# 1. Add & change
snake = {"species": "burmese", "length_ft": 12, "venomous": False}
snake["docile"] = True
snake["length_ft"] = 16
print(snake)
# 2. Remove
snake = {"species": "burmese", "length_ft": 16, "venomous": False}
del snake["venomous"]
print(snake)
# 3. Loop and collect
snake = {"species": "burmese", "length_ft": 16, "venomous": False}
values = []
for value in snake.values():
values.append(value)
print(values)
# 4. Nested access
snakes = {
"burmese": {"length_ft": 16, "venomous": False},
"ball": {"length_ft": 5, "venomous": False},
}
print(snakes["ball"]["length_ft"])
Extending dicts with collections
A plain dict can tally counts or group items, but both take extra setup code: checking
whether a key exists before incrementing it, or before appending to a list under it. The
collections library adds several dicts that handle cases
like these automatically.
Countercounts items in a sequence directly — reach for it as soon as a dict's job is "how many times does each item show up."defaultdictsupplies an empty value (a list, a set,0) the first time a new key is used, so grouping items under keys that aren't known ahead of time doesn't need anif key not in dictcheck before every write.OrderedDictis worth reaching for only when order itself needs to be compared or reordered — a plain dict already remembers insertion order, but its==ignores that order, and it has nomove_to_end().ChainMaplayers several dicts together — like a set of overrides checked before a set of defaults — without copying or merging them into a new dict.
See the collections library page for the full method list on each of these.
Tuples
A tuple stores multiple items, in order, written in parentheses. They are immutable so the items can't be changed once its created.
species = ("burmese", "rock", "ball", "blood")
block-beta
block:diagram
columns 5
lblValue["Value"] v0[""burmese""] v1[""rock""] v2[""ball""] v3[""blood""]
lblIndex["Index"] i0["0"] i1["1"] i2["2"] i3["3"]
lblNeg["Negative index"] n0["-4"] n1["-3"] n2["-2"] n3["-1"]
end
classDef label fill:none,stroke:none,color:#8A8370
classDef panel fill:#23221E,stroke:#35342E,stroke-width:1px
class i0,i1,i2,i3,n0,n1,n2,n3,lblValue,lblIndex,lblNeg label
class diagram panel
FIG: tuple values with their positive and negative indexes
The index of the first item is 01, next is 1, and so on.
The negative index starts counting down from the end instead, starting at -1 for the last item, -2 for the second-to-last, and so on. Each item can be referenced by its positive or negative index.
Access items
-
Index with
tuple[index].species[0] # "burmese" species[-1] # "blood" -
A slice
tuple[start:end]returns a new tuple containing items fromstartindex up to (but not including) theendindex.species[1:3] # ("rock", "ball")
Loop through a tuple
-
The loop runs once for every item in the tuple, and on each pass the loop variable, (i.e.
specie) is set to the next item in the tuple.for specie in species: print(specie) # burmese rock ball blood -
If you also want the index alongside the item,
enumerate()hands back both together — works the same as on a list, since tuples support indexing too.for index, specie in enumerate(species): print(index, specie) # 0 "burmese", then 1 "rock", then 2 "ball", then 3 "blood"
Boolean expressions
-
inchecks whether a value exists in the tuple."rock" in species # True -
not inchecks whether a value is missing from the tuple."carpet" not in species # True -
==checks whether two tuples have the same contents, in the same order.!=checks whether they differ. Or compare a specific item directly, likespecies[0] == "burmese".other_species = ("burmese", "rock", "ball", "blood") species == other_species # True species != ("burmese",) # True -
ischecks whether two variables point to the exact same tuple object, not just an equal one — use==to compare contents.same_species = species # another name for the same tuple species is same_species # True, same tuple species is ("burmese", "rock", "ball", "blood") # False, different tuple with equal contents -
boolean expression:
-
Truthy: a tuple with contents
-
Falsy: an empty tuple
()
if species: # runs — species tuple has items print("found some") -
Packing and unpacking
-
Packing: writing several values separated by commas, with or without the surrounding parentheses, implicitly builds a tuple.
species = "burmese", "rock", "ball", "blood" # parentheses optional — still a tuple type(species) # <class 'tuple'> -
Unpacking: assigns each item in a tuple to its own variable in one line. The number of variables has to match the number of items.
a, b, c, d = species # a="burmese" b="rock" c="ball" d="blood"A
matchstatement can do this same unpacking while also branching on the tuple's shape or specific values.snake = (12, "ball") match snake: case (length, "ball"): # tuple with 2 items, where second is "ball" print(f"a {length} ft ball python") # in this example, this case will run case (length, specie): # tuple with any 2 items print(f"a {length} ft {specie} python") case (length,): # tuple with any 1 item print(f"just a length: {length}") case _: # 0 items, or tuple with more than 2 items print("invalid format") -
Swapping variables: unpack two values into each other's variables in one line, instead of using a temporary variable to hold one during the swap.
a, b = "ball python", "boa" # a="ball python" b="boa" a, b = b, a # swaps directly — no temporary variable needed — a="boa" b="ball python"
Tuple operations
Inspect
-
len()returns how many items are in a tuple.len(species) # 4 -
count()counts how many times a value appears.species.count("burmese") # 1 -
index()finds the position of the first match.species.index("ball") # 2
Arithmetic
-
min()finds the smallest item.length_ft = (12, 4.5, 3.5, 5) min(length_ft) # 3.5 -
max()finds the largest item.max(length_ft) # 12 -
sum()adds every item together.sum(length_ft) # 25
Convert to modify
-
Convert to list -> edit -> convert back to tuple builds a new tuple since a tuple can't be edited directly.
species_list = list(species) # convert tuple to a list species_list.append("carpet") # edit it like any list species = tuple(species_list) # convert back to tuple and reassign
Create
-
tuple()builds the same tuple from any iterable, if you'd rather not use literal parentheses.tuple(["burmese", "rock", "ball", "blood"]) # ("burmese", "rock", "ball", "blood")
Going further
Practice with tuples
Each box below is fully editable — write your answer, then click Run.
1. Access & check membership. Print the last item, then check whether "carpet" is in the tuple.
species = ("indian", "carpet", "rock", "blood")
# your code here
2. Unpacking. Unpack the tuple into four variables named a, b, c, d, then print them.
species = ("indian", "carpet", "rock", "blood")
# your code here
3. Work around immutability. Tuples can't be appended to directly — build a new tuple with "angolan" added to the end, using +.
species = ("indian", "carpet", "rock", "blood")
# your code here
print(species)
Show solutions
# 1. Access & check membership
species = ("indian", "carpet", "rock", "blood")
print(species[-1])
print("carpet" in species)
# 2. Unpacking
species = ("indian", "carpet", "rock", "blood")
a, b, c, d = species
print(a, b, c, d)
# 3. Work around immutability
species = ("indian", "carpet", "rock", "blood")
species = species + ("angolan",)
print(species)
Extending tuples with collections.namedtuple
A plain tuple's items can only be accessed by position — snake[1] doesn't say what
that value actually means without checking back how the tuple was built. The
collections library's
namedtuple builds a tuple type with named fields,
so the same value reads as snake.length_ft. Switch to it once a tuple's positions start
needing a mental lookup table to remember, or once several tuples share the same shape
throughout a program — a single namedtuple definition documents that shape once instead
of repeating a comment at every literal.
See the collections library page for namedtuple's other
methods (_asdict(), _replace(), default field values) and the rest of the module.
Sets
A set stores multiple items, in no particular order, inside a single variable — written with curly braces.
Because items have no fixed position, there's no indexing. Duplicates are irrelevant because adding a value that's already there changes nothing; a set can only ever hold each value once.
species = {"burmese", "rock", "ball", "blood"} # order not fixed
block-beta
block:diagram
columns 5
lblValue["Unordered set: "] v0[""burmese""] v1[""rock""] v2[""ball""] v3[""blood""]
end
classDef label fill:none,stroke:none,color:#8A8370
classDef panel fill:#23221E,stroke:#35342E,stroke-width:1px
class lblValue label
class diagram panel
FIG: set values with no fixed order
Loop through a set
The loop runs once for every item in the set, in no guaranteed order, and on each pass the loop variable, (i.e. specie) is set to the next item.
for specie in species:
print(specie) # burmese rock ball blood — order not guaranteed
Boolean expressions
-
inchecks whether a value exists — and does it far faster than a list or tuple, no matter how large the set gets, since Python looks it up directly instead of scanning item by item."burmese" in species # True -
not inchecks whether a value is missing from the set."cobra" not in species # True -
==checks whether two sets have the same contents, regardless of order.!=checks whether they differ.other_species = {"burmese", "rock", "ball", "blood"} species == other_species # True -
ischecks whether two variables point to the exact same set object, not just an equal one — use==to compare contents.same_species = species # another name for the same set copy_of_species = species.copy() # a separate set, equal contents species is same_species # True, same set species is copy_of_species # False, different set with equal contents -
issubset(),issuperset(),isdisjoint()compare two sets and hand back abooltoo — see Set operations: Compare for the full rundown. -
boolean expression:
-
Truthy: a set with contents
-
Falsy: an empty set — written
set(), not{}, since{}creates an empty dict instead
if species: # runs — the set has items print("found some") while species: # loops until the set is empty species.pop() -
Set operations
Inspect
-
len()returns how many items are in a set.len(species) # 4
Arithmetic
-
min()finds the smallest item.length_ft = {12, 4.5, 3.5, 5} min(length_ft) # 3.5 -
max()finds the largest item.max(length_ft) # 12 -
sum()adds every item together.sum(length_ft) # 25
Update
-
add()adds a single item. Adding a value that's already present changes nothing.species.add("carpet") # {"burmese", "rock", "ball", "blood", "carpet"} species.add("burmese") # already there — no change -
update()adds every item from another iterable, one at a time.species.update(["carpet", "boa"]) # adds "boa"; "carpet" was already there
Remove
-
remove()deletes an item, raising an error if it isn't there.species.remove("rock") # errors if "rock" isn't in the set -
discard()does the same but stays silent if the item's missing.species.discard("rock") # no error either way -
pop()removes and returns an arbitrary item, since there's no "last" item in an unordered collection.species.pop() # removes and returns *some* item — which one isn't guaranteed -
clear()empties the set.species.clear() # set()
Combine
Sets support the same operations as sets in math class — useful for comparing two groups directly instead of writing your own loop to do it.
constrictors = {"ball", "burmese", "boa"}
pet_friendly = {"ball", "burmese", "corn snake"}
-
|union — everything in either set.constrictors | pet_friendly # {"ball", "burmese", "boa", "corn snake"} -
&intersection — only what's in both.constrictors & pet_friendly # {"ball", "burmese"} -
-difference — in the first set, but not the second.constrictors - pet_friendly # {"boa"} -
^symmetric difference — in one set or the other, but not both.constrictors ^ pet_friendly # {"boa", "corn snake"}
Compare
These check a relationship between two sets and hand back a bool, rather than building a new set the way Combine does.
-
issubset()checks whether every item in this set is also in another set.{"ball", "burmese"}.issubset(constrictors) # True -
issuperset()checks whether this set contains every item in another set — the reverse ofissubset().constrictors.issuperset({"ball"}) # True -
isdisjoint()checks whether two sets have no items in common.constrictors.isdisjoint({"cobra", "viper"}) # True
Create
-
set()builds the same set from any iterable, if you'd rather not use literal braces.set(["burmese", "rock", "ball", "blood"]) # {'burmese', 'rock', 'ball', 'blood'} -
copy()makes a real, independent copy of the set — unlikenew_set = old_set, which just points a second name at the same set, so a change through either name shows up in both.same_set = species # same_set and species are the same set — mutating one mutates both backup = species.copy() # backup is a separate, independent set
Removing duplicates from a list
Converting a list to a set and back is a common one-line way to drop duplicates — though it also throws away the original order, unless you sort or otherwise re-derive it.
species = ["ball", "burmese", "ball", "boa", "burmese"]
list(set(species)) # ["burmese", "ball", "boa"] — order not guaranteed
For efficiency, use a set instead of a list for membership checks
| Time | Space | |
|---|---|---|
List/tuple in |
O(n) | — |
Set/dict in |
O(1) average | — |
Checking in on a list or tuple is O(n) — worst case, Python has to look at every item before it can say no. A set (and a dict, checking its keys) looks a value up directly instead of scanning, so in on either is O(1) on average, regardless of size. That's the "far faster" mentioned above, named precisely — it's also the reason converting a list to a set is a common move before doing a lot of membership checks against it.
See Efficiency for why this distinction matters.
Going further
Practice with sets
Each box below is fully editable — write your answer, then click Run.
1. Check membership & add. Check whether "carpet" is in the set, then add it.
species = {"indian", "rock", "blood", "angolan"}
# your code here
2. Remove. Discard "rock" from the set — using the method that won't error even if it's already gone.
species = {"indian", "rock", "blood", "angolan"}
# your code here
print(species)
3. Deduplicate. Given the list below (with repeats), build a set from it to remove duplicates, then convert it back to a list.
names = ["ball", "burmese", "ball", "boa", "burmese"]
# your code here
print(unique_names)
Show solutions
# 1. Check membership & add
species = {"indian", "rock", "blood", "angolan"}
print("carpet" in species)
species.add("carpet")
# 2. Remove
species = {"indian", "rock", "blood", "angolan"}
species.discard("rock")
print(species)
# 3. Deduplicate
names = ["ball", "burmese", "ball", "boa", "burmese"]
unique_names = list(set(names))
print(unique_names)
-
In programming, counting generally starts at 0, not 1. That's because an index isn't really a count of "how manyth" item something is — it's an offset, the number of steps from the start. The first item is 0 steps away, so it gets index
0. It feels different from counting out loud ("first, second, third..."), but it's the convention nearly every programming language follows. ↩↩