<jupyter_start><jupyter_text>Data StructuresIn this notebook, we'll explore different types of data structures that Python can use to store information, namely **lists, tuples, and dictionaries.**  At the end of this notebook, you'll be able to:* Compare & contrast the types of structures that Python uses to store data points* Recognize & create lists, tuples, and dictionaries in Python* Index, slice, cast, and mutate lists* Understand the implications of mutability and object-oriented programming  ListsA _list_ is a mutable collection of ordered items, that can be of mixed type.**Mutable** means that individual items in the object can be changed. Lists are mutable. Tuples and strings are not -- they're **immutable**.Lists are created using square brackets `[ ]`, and individual elements are separated by commas.<jupyter_code># Create a list of fruits
my_fruits = ['mango','banana','lychee','cherries','grapes']
my_fruits<jupyter_output><empty_output><jupyter_text>Useful list methods- Check the length of your list by using `len(my_list)`- Use `my_list.append()` to add elements to a list- Remove elements by index using `del my_list[index]`- Remove elements by value by using `my_list.remove('value')`- Sort by using `my_list.sort()`<jupyter_code># Check length 
len(my_fruits)
# Append to fruits
my_fruits.append('lychee')
my_fruits
# Remove fruits by value
my_fruits.remove('banana')
my_fruits
# Sort fruits alphabetically
my_fruits.sort()
my_fruits<jupyter_output><empty_output><jupyter_text>List indexing & slicing**Indexing** refers to selecting an item from within a collection (e.g., lists, tuples, and strings). Indexing is done by placing the **index number** in square brackets, directly after the list variable.For example, if `my_list = [1,3,5]`, we can get the second value using `my_list[1]`. (Remember that Python starts indexing at zero!)  Reminders- Python is zero-based (The first index is '0')- Negative indices index backwards through a collection<jupyter_code># Try indexing our list of fruits here
last_fruit = my_fruits[-1]
last_fruit<jupyter_output><empty_output><jupyter_text>If we want multiple items, we can **slice** the list.There are a few ways to slice:1. We can **slice** a part of a list using the syntax `[start:stop]`, which extracts characters between index start and stop (-1).**Notes**- `start` is __included__ then every element __until__ `stop` is included.- Negative values count backwards through the list. 2. If we omit either (or both) of start or stop from `[start:stop]`, the default is the beginning and the end of the string, respectively, e.g. `[:3]`3. We can also define the step size (instead of default 1) using the syntax `[start:stop:step]` Task: For our list of fruits, create three different slices, and save them as different variables:    1. A slice of the first two fruits.2. A slice of the middle three fruits.3. A slice of the last fruit.<jupyter_code># Your code here!
first_two = my_fruits[:2]
middle_three = my_fruits[1:4]
last_fruit = my_fruits[-1]

print(my_fruits)
print(first_two)
print(middle_three)
print(last_fruit)<jupyter_output>['mango', 'banana', 'lychee', 'cherries', 'grapes', 'lychee']
['mango', 'banana']
['banana', 'lychee', 'cherries']
lychee<jupyter_text>Checking lengthWe can use the function `len( )` to check the length of lists.**Note**: We can also use this to get the number of characters in a string!<jupyter_code>len(middle_three)<jupyter_output><empty_output><jupyter_text>Checking membershipWe can use `in` to see if an item exists in a list. The `in` operator checks whether an element is present in a collection, and can be negated with `not`. _(More on operators in the next lecture)_<jupyter_code>'watermelon' not in my_fruits<jupyter_output><empty_output><jupyter_text>Mutating listsAfter definition, we can update members of our list _because lists are mutable!_ This also impacts aliases of our lists.<jupyter_code>my_list = [1,2,3,4,5]

alias_my_list = my_list

my_list[0] = 8

alias_my_list
print(id(alias_my_list))
print(id(my_list))
copy_my_list = my_list.copy()

my_list[0] = 800

copy_my_list
print(id(copy_my_list))<jupyter_output>140527350951496<jupyter_text>Creating lists of listsSometimes, it's useful to create lists of lists. Often, if we import big datasets as lists, this is how it will be organized.![](https://media.giphy.com/media/z1meXneq0oUh2/giphy.gif)<jupyter_code>gene_1 = ['gene1',0.48,0.55]
gene_2 = ['gene2',0.38,0.85]
gene_3 = ['gene3',0.21,0.81]
all_genes = [gene_1, gene_2, gene_3]

#print(all_genes)

# We can use this syntax to get a specific value
print(all_genes[2][0])<jupyter_output>gene3<jupyter_text>TuplesA _tuple_ is an **immutable** collection of ordered items, that can be of mixed type.* Tuples are created using parentheses.* Indexing works similar to lists.<jupyter_code># Define a tuple
my_tuple = ('a',1,3.4)
my_tuple[0]<jupyter_output><empty_output><jupyter_text>Question: Before running the cell below, try to predict: What will be printed out from running this code?<jupyter_code>lst = ['a', 'b', 'e']
tup = ('b', 'c', 'd')

if lst[-1] == tup[-1]:
    print('EndMatch')
elif tup[1] in lst:
    print('Overlap')
elif len(lst) == tup:
    print('Length')
else:
    print('None')<jupyter_output>None<jupyter_text>Casting between variable typesWe can use `list( )` or `tuple( )` to convert variables into different types. This is called **casting**.This is particularly useful when we use an operator like `range( )` which generates a range, but in the form of an **iterator**. **Note**: `range`, like indexing, is defined with `start`,`stop`, and `step`, but commas in between each. Remember that you can always use `?range` or `help(range)` to get details on how a function works.<jupyter_code># Test range here

my_range = range(2,11,2)
my_range_list = list(my_range)
my_range_list<jupyter_output><empty_output><jupyter_text>DictionariesDictionaries are also like lists, except that each element is a key-value pair. The syntax for dictionaries is `{key1 : value1, ...}:` When dictionaries are useful1. Flexible & efficient way to associate labels with heterogeneous data2. Use where data items have, or can be given, labels3. Appropriate for collecting data of different kinds (e.g., name, addresses, ages)> In the cell below, create a dictionary for three countries and capitals using the syntax `{country:capital,...}`. Remember that strings still need parentheses!**Note**: You can also create an empty dicitionary using `{}` and fill it using `dictionary['key'] = 'value'`.<jupyter_code>capitals = {'United Kingdom':'London','France':'Paris','Spain':'Madrid'}
capitals
capitals['United States'] = 'Washington DC'
capitals<jupyter_output><empty_output><jupyter_text>Question: Before running the cell below, predict: What would the following code produce?<jupyter_code>capitals.update({'Egypt':'Cairo'})
capitals<jupyter_output><empty_output><jupyter_text>Task: What happens if we look for a key that doesn't exist? Try this above.<jupyter_code>capitals['South Africa']
del capitals['United Kingdom']
capitals<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10843543162934798, 'fraction_numerical': 0.01323757217293339, 'mean_word_length': 6.038652130822596, 'pattern_counts': {'":': 0, '<': 53, '<?xml version=': 0, '>': 54, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '637609', 'license': 'CC0-1.0', 'n_tokens_mistral': 2317, 'n_tokens_neox': 2219, 'n_words': 836, 'path': 'Solutions/04-DataStructures_solutions.ipynb', 'repo_name': 'BIPN62/Materials', 'size': '7101'}