--- title: Linux & scripting for data wrangling author: Jimmy Christensen theme: Linux, Bash, Python date: October 31, 2022 duration: 120 --- # Overview - Linux - Bash - Python Slides available at https://dusted.dk/pages/scripting/slides.html # Linux ? Linux is an operating system, like Windows or OSX, it allows you to run programs. # Bash ? Popular "command line" for Linux & OSX - Launch programs - Interact with files and folders - Composition via pipes - Scripting language - Easy things, very easy - Medium things, hard - Hard things, very hard # Python ? Popular scripting language - Easy to learn - General purpose - Easy things are easy enough - Medium things are not so bad - Hard things are less hard # All of them ? Bash, grep, awk, sed, tr, cut: - Very fast - Easy things are easy - Everything else is really hard - Use for rough extraction Bash scripts using above programs to automate extraction. Python scripts to remodel and consolidate extracted data, calculate and prepare results. # Overview: Linux - Editing with nano - Files / Directories - Pipes & redirects # Editing with nano Nano is a (bit too) simple text editor - nano myfile.sh - ctrl+o to op... save! - ctrl+w to wri.. search! - ctrl+k to k.... cut! - ctrl+u to yes.. insert! ;-) - ctrl+x to eXit # Files / Directories /home/chrisji/theWork/important.txt - pwd - cd - ls -lah - hd, head, tail, more, du, wc Peek at unknown file: hd strangeFile | head Check size: ls -lh strangeFile Only beginning: head strangeFile Page through it: more strangeFile # Pipes & redirects - Programs will read data from standard input (stdin) - Programs will write data to standard output (stdout) - Pipe directs stdout into the next programs stdin - programA | programB | programC - \> and >> directs stdout to a file # Pipes & redirects Composition, also useful in bash scripting ```bash # A pipe directs data from stdout of one program to stdin of the next ls | wc -l # A redirect directs data from stdout of a program to a file. ls > myFiles.txt # An appending redirect will not truncate the file first. echo "Line 1" > file.txt echo "Line 2" >> file.txt ``` # Script vs program Same difference, almost - Scripts typically easier to write for the human - Scripts typically slower to execute for the computer - Scripts are "clear text" # Overview: Bash - Making & running a bash script - Variables & parameters - Conditionals & loops - Reading data - Functions - Debugging - Tips # Two quick notes ```bash # This line is a comment, because it starts with the hash mark # comments are to help the humans (us) both bash and python # ignores a hash mark and any following text but only for rest of that line ``` - Scripts are executed by the computer one line at a time, not "all at once". # Making & running a bash script Creating the script with the nano editor ```bash nano shee.sh ``` ![](./bash1.png) - ctrl +o to save the file - ctrl +x to exit nano # Making & running a bash script ```bash # Look at our new script file, it is not marked as being "executable" ls -l shee.sh # We make it executable! chmod +x shee.sh # Check that it has the x flag (and pretty green color) ls -l shee.sh # Now we can run it ./shee.sh Hello World! ``` ![](./bash2.png) # Bash: Variables ![](var.png) Holds something, has a name, like box with a label on it ```bash #!/bin/bash placeToGreet="World" echo "Hello $placeToGreet!" ``` ``` Hello World! ``` # Bash: Variables ![](var.png) They can be re-assigned! ```bash #!/bin/bash placeToGreet="World" placeToGreet="Earth" echo "Hello $placeToGreet!" ``` What's the result ? (remember, line-by-line) # Bash: Variables A bash variable can store about 2 megabytes of data, python about 64 gigabytes ```bash #!/bin/bash # Create some variables and assign them values timesToCall=3 animal="dog" name="viggo" # Output to the screen echo "Calling for $name the $animal $timesToCall times!" ``` ``` Calling for viggo the dog 3 times! ``` # Bash: Parameters Variables assigned from command-line ```bash # The shee.sh script is executed with four parameters: # First parameter: 3 # Second parameter: dog # Third parameter: viggo # Fourth parameter: heere doggie doggie! ./shee.sh 3 dog viggo "heere doggie doggie!" Calling for viggo the dog 3 times! ``` # Bash: Parameters ```bash #!/bin/bash # $1 = first parameter # $2 = second parameter timesToCall=$1 animal="$2" name="$3" echo "Calling for $name the $animal $timesToCall times!" ``` # Variables: There's more Bash has strong string-manipulation capabilities, quickly degenerates into alphabet soup Don't go there, use python For the curious: https://tldp.org/LDP/abs/html/string-manipulation.html It also has arrays and sets, bash syntax makes it painful # Conditionals Conditionals let's us decide if we want to do something ```bash if CONDITION then # do # some # things fi ``` # Conditionals The "else" word lets us do something if the condition is not met ```bash if CONDITION then # do # some # things else # do some # other # things fi ``` # Conditionals The elif is optional, allows chains ```bash if CONDITION_A then # something to do if A.. elif CONDITION_B then # This must not happen if A, but if B. elif CONDITION_C then # If neither A nor B, but C, then do this else # If not A, B nor C, then do this.. fi ``` else is also optional ```bash if CONDITION_A then # Do this if A elif CONDITION_B then # Do this if B fi ``` # Conditionals In bash, a condition is met if the [ ] builtin or a program exit with the "success" status code. ```bash true # A program that returns success false # A program that returns.. not success grep # Will return success if it finds at least one match [ "$name" == "viggo" ] # Will success if the variable contains the text viggo [ $a -gt $b ] # success if a is Greather Than b [ $a -lt $b ] # success if a is Less Then b [ $a -eq $b ] # success if a is EQual to b [ -z ${a+x} ] # success if a is empty (weird syntax, I know) [ "$name" == "viggo" ] && [ "$animal" == "dog" ] # success only when name is viggo AND animal is dog (both must be true) [ "$name" == "viggo" ] || [ "$animal" == "dog" ] # success if name is viggo or animal is dog (one or both must be true) ``` # Conditionals ```bash if echo "secret string" | grep "secret" &> /dev/null then echo "The secret string was found!" fi ``` # Conditionals ```bash if [ "$name" == "viggo" ] && [ "$animal" == "dog" ] then echo "Viggo is a very special dog!" elif [ "$animal" == "dog" ] then echo "Dogs are just great, even if they're $name instead of viggo!" else echo "I'm sure $name is a great $animal!" fi ``` # Loops - for loop - while loop - do while loop (rarely used) # Loop: for Repeats for each item in sequence ```bash for VARIABLE in SEQUENCE do # things to do done ``` # Loop: for Example ```bash for number in 1 2 3 4 do echo "Number is $number" done ``` # Loop: for ```bash animals="dog fish cat bird" for animal in $animals do echo "$animal is an animal" done ``` ``` dog is an animal fish is an animal cat is an animal bird is an animal ``` # Loop: for ```bash animals="dog fish cat bird" for animal in "$animals" do echo "$animal is an animal" done ``` ``` dog fish cat bird is an animal ``` # Loop: for Use expansion for files, this produces a sequence of .txt files in current directory ```bash for file in *.txt do echo "Text file: $file" done ``` # Loop: while Repeats as long as CONDITION is met (potentially forever) ```bash while CONDITION do # thing to do done ``` # Interjection This is a good time to remind you that pressing ctrl+c (eventually) terminates a running script. # Loop: while When you need to repeat something until "things are done" ```bash while CONDITION do # Thing to do until CONDITION is no longer met done ``` # Loop: while ```bash while read line do echo "This just in: $line" done ``` # Reading data - From human - From file - From program - From stream # Reading data: The read builtin ```bash read VARIABLE # Creates a variable and reads data from standard input into it ``` # Read data from human Useful for interactive scripts ```bash echo "Welcome to super cool script v 24!" # The -p parameter is a prompt to show to the human read -p "File to corrupt: " fileName echo "Don't worry, I was just kidding, nothing happened to $fileName" ``` ``` Welcome to super cool script v 24! File to corrupt: homework.md Don't worry, I was just kidding, nothing happened to homework.md ``` # Read data from human (in a loop) ```bash answer="" while [ "$answer" != "y" ] && [ "$answer" != "n" ] do read -p "Continue? [y/n] ?" answer done if [ "$answer" == "n" ] then echo "Stopping here." exit fi echo "Continuing" ``` ``` Continue? [y/n] ?yes Continue? [y/n] ?... Continue? [y/n] ?yeees Continue? [y/n] ?noooo ? Continue? [y/n] ?n Stopping here. ``` # Read data from file For small files, we can read the entire file into a variable by running the cat program and capturing its output in a variable ```bash someFileContents=$(cat secrets.txt) echo "File content: $someFileContents" ``` # Read data from program For small outputs, we can capture the entire standard (non-error) output into a variable ```bash gloriousResult=$(grep 'someRegex' secrets.txt) ``` # Read data from stream Read exits with success unless standard input is closed ```bash grep 'someRegex' secrets.txt | while read line do echo "A line of the result: $line" done ``` # Read data from stream We can compose complex pipelines now ```bash grep 'someRegex' secrets.txt | while read line do # Extract some field from the line importantThing=$(echo $line | cut -f 2 -d ',' | awk magic things) # Look for that field in some other file, put the result in a variable if resultFromOtherFile=$(grep $importantThing otherSecrets.txt) then # Eureka! some correlation is interesting when the field from subset of secrets.txt is in otherSecrets.txt! # Maybe combine on one line and print it out? echo "$line,$resultFromOtherFile" fi done > results.txt ``` # Read data from stream We can also write a script that takes a stream from a pipe, for use with other scripts ourCoolScript.sh ```bash #!/bin/bash while read ourLine do # Do intersting things with ourLine done ``` We'd use it like this ```bash grep 'someRegex' someFile.txt | ./ourCoolScript.sh > results.txt ``` # Write data We just use the > and >> after any command, or after the script itself. ```bash > # Will create an empty file (or truncate an existing one) >> # Will create an empty file (or append to an existing one) ``` # Functions - A small "script inside your script" - Can take parameters - Useful for repetitive tasks (don't repeat yourself) # Functions ```bash function removeRepeatSpaces { tr -s " " } echo "Badly formatted data right here." | removeRepeatSpaces ``` ```bash function largestOne { if [ $1 -gt $2 ] then echo $1 elif $1 -lt $2 ] then echo $2 fi } largest=$(largestOne $A $B) ``` # Debugging See _EVERYTHING_ that happens during script execution by adding set -x ```bash #!/bin/bash set -x echo "My script" read -p "What is your name ?" name if [ "$name" == "viggo" ] then echo "Good dog!" fi ``` ![](./bash3.png) # Tips - Comment - Indent - Use good variable names - Filter before iterating - Do as much filtering as is convenient, but not more - Google # Tips - If script takes parameters, have it write how to use it ```bash #!/bin/bash # This script needs 3 paramters, name, animal and number of legs name=$1 animal=$2 nlegs=$3 if [ -z ${nlegs+x} ] then echo "Usage $0 NAME ANIMAL NUM_LEGS" echo " NAME - The name of the pet (example: viggo)" echo " ANIMAL - What kind of animal is it? (example: dog)" echo " NUM_LEGS - How many legs does it have? (example: 4)" exit 1 fi ``` Even if you're the only user, you might forgot how to use it later $0 is special, it is always the name of the script in question. # Tips Suggestion for treating multiple files while keeping track of the source of each result Let there be binary files data1.bin data2.bin ... dataN.bin that can be translated by a "translatebinarytotext" program and filtered by some regular expression to find relevant lines: ```bash #!/bin/bash for fileName in data*.bin do translatebinarytotext "$fileName" \ | grep 'someRegex' \ | ./ourAwesomeScript.sh "$fileName" \ >> result.txt done ``` ```bash # Alternative if the files are unorderly for fileName in data1.bin data_2.bin thirdPartOfData.bin ``` The polished version of above alternative could be used like a real program. ```bash ./processThePeskyFiles.sh data1.bin data_2.bin thirdPartOfData.bin > result.txt ``` Then the script would look like: ```bash #!/bin/bash for fileName in $@ do translatebinarytotext "$fileName" \ | grep 'someRegex' \ | ./ourAwesomeScript.sh "$fileName" done ``` # Tips It can be useful to write out such "debug" information to the standard error channel, this way it goes "around" pipes and are shown on screen. ```bash #!/bin/bash # This function writes a message to stderr, so our script output can be piped while we still see information/errors. function msg { echo $@ >&2 } msg "Script for doing cool stuff running in $(pwd)" echo "Important data output" ``` ``` bash coolStuffDoer.sh > test.txt Script for doing cool stuff running in /home/chrisji/theWork/ cat test.txt Important data output ``` # Tips Script and data does not need to be the same place, you can write the script as if it is next to the files. ```text . ├── data │   └── seta │   └── wool.txt └── scripts └── sheep.sh ``` scripts/sheep.sh: ```bash #!/bin/bash sheep=$(grep sheep wool.txt | wc -l) echo "There are $sheep sheep." ``` ```bash cd data/seta ../../scripts/sheep.sh wool.txt ``` # Tips Alternative, write script that uses parameters: ```text . ├── data │   └── seta │   └── wool.txt └── scripts └── sheep.sh ``` scripts/sheep.sh: ```bash #!/bin/bash file="$1" sheep=$(grep sheep "$file" | wc -l) echo "There are $sheep sheep in $file" ``` ```bash ./scripts/sheep.sh data/seta/wool.txt ``` # Tips Alternative, write script that uses pipes: ```text . ├── data │   └── seta │   └── wool.txt └── scripts └── sheep.sh ``` scripts/sheep.sh: ```bash #!/bin/bash sheep=$(grep sheep | wc -l) echo "There are $sheep sheep." ``` ```bash cat data/seta/wool.txt | ./scripts/sheep.sh ``` # Tips A script that reads which files to work on from a file filesToWorkOn.txt ``` file1.txt file2.txt file3.txt ``` process.sh ```bash #!/bin/bash listFileName="$1" filesToProcess=$(cat "$listFileName") for fileToProcess in $filesToProcess do echo "Processing file: $fileToProcess" done ``` ``` ./process.sh filesToWorkOn.txt Processing file: file1.txt Processing file: file2.txt Processing file: file3.txt ``` # Overview: Python - Make a script & run it - Variables & parameters - Conditionals & loops - Functions - Reading & writing, files & pipes # Why?! - Easy and powerful math - Easy and powerful text string manipulation - Easier syntax - Easier to work with sets and lists - Nice modules for science stuff # Make a script & run it ```bash nano hello.py ``` ```python #!/usr/bin/python3 print("Hello World!") ``` - ctrl + o - ctrl + x ```bash ./hello.py Hello World! ``` # Variables ```python #!/usr/bin/python3 placeToGreet = "Earth" # Apologies, the f is not a typo, it tells python we want variables substituted in the text string print(f"Hello World {placeToGreet}!") ``` # Variable Types & objects # Variables - Text strings - Numbers - Lists - Sets - Dictionaries - Powerful functions to easily work with them - Convenient regex and sub-strings # Variables ```python # integer length=23 # floating point pi=3.1415 tau=pi*2 # set instances={ pi, tau, tau, 'dog', 'viggo' } # How many tau is in the set? 1. It's a set. # What order are the items in? None, sets are unordered. # Convenient for acumulating unique occurences of stuff instances.add( 42 ) if tau in instances: print("Tau is in the set!") # list aList = [ 5,3,12,5 ] # Lists are ordered, duplicates are allowed. (5 occurs twice, at the start and end) # In python and most other languages, the first element of a list is 0 print(f"The second element in the list is {aList[1]}) ``` # Variables Dictionaries makes it easy to structure data ```python dogs = dict( viggo = dict( age = 4, legs= 4, tail=true ), henry = dict( age = 9, legs= 3.9, tail=true ) ) dogs.update( bob = dict( age = 2, legs= 4, tail=false # poor guy! ) ) print( f"Viggo is {dogs['viggo']['age']} years old." ) ``` # Variables ```python # Sets can accumulate unique things, dicts can be count them numAnimals = dict() # -- snip -- if 'tau' not in numAnimals: numAnimals['tau'] = 0 numAnimals['tau'] += 1 ``` # Variables More than everything you need to know about Python variables at [https://docs.python.org/3/library/stdtypes.html](https://docs.python.org/3/library/stdtypes.html) # Conditionals Conditionals easier to read in python, generally we don't run external programs, so the boolean result of an expression is all we worry about. A condition is met when the result is True or any non-zero value. A condition is not met when the result is False or 0, or empty (such as the empty string "" or list []) # Conditionals ```python a > b # Is True if a is greater than b a < b # Is True if a is less than b a == b # Is True if a is equal to b a != b # Is true if a is not euqal t b a < b and b > c # IS true if a is less than b AND b is greater than c a > b or b < c # Is true if a is greater than b or if b is less than c a in c # Is true if a is an element in c a not in c # Is true if a is not an element in c ``` # Conditionals ```python if CONDITION: # Do something that should be done # if CONDITION is met ``` ```python if CONDITION # Do something else # Do something else ``` # Attention! Python is indentation sensitive, that is, the number of "tabs" indicate to which code-block a line belong. ```python if False: print("This is never shown") print("And this is also never shown") print("This is shown") ``` # Conditionals ```python name = 'viggo' if name == 'viggo': print(f"The dogs name is Viggo!") ``` # Conditionals ```python name = 'Viggo' # 'Viggo' is not the same as 'viggo', but python strings provide a method for lowercasing for comparisons if name.casefold() == 'viggo': print(f"The dogs name is Viggo!") ``` # Loops ```python # For is great for iteration over "iterables" for VARIABLE in ITERABLE: # do thing for each item ``` ```python for number in range( 10, 20 ): print(f"Number: {number}") ``` ``` Number: 10 Number: 11 Number: 12 Number: 13 Number: 14 Number: 15 Number: 16 Number: 17 Number: 18 Number: 19 ``` More about the CSV module at [https://docs.python.org/3/library/csv.html](https://docs.python.org/3/library/csv.html) # Loops ```python #!/usr/bin/python3 import sys import csv # This csv file used ; instead of , reader = csv.reader(sys.stdin, delimiter=';') rowNumber=0 for row in reader: name=row[0] age=int(row[1]) # Convert from text to number print(f"Row {rowNumber}, name: {name} age: {age}") if age > 5: print(f"{name} is an old doggie!") rowNumber += 1 ``` ```bash cat csvfi | ./hello.py Row 1, name: viggo age: 6 viggo is an old doggie! Row 2, name: henry age: 4 ``` # Loops Much more about loops here: [https://wiki.python.org/moin/ForLoop](https://wiki.python.org/moin/ForLoop) # Reading from a file With an open file, readline will read a single line from the file, useful for large files ```python #!/usr/bin/python3 fileName="poe.txt" with open(fileName) as textFile: while True: line = textFile.readline() if not line: # If the line was empty, file end reached break; # so break the lop line = line.strip(); print(f"{line}") ``` ``` Once upon a midnight dreary, while I pondered, weak and weary, Over many a quaint and curious volume of forgotten lore— ``` # Reading from a file With an open file, readlines will read from the file into a list ```python #!/usr/bin/python3 lineNumber=0 fileName="poe.txt" with open(fileName) as textFile: lines = textFile.readlines() for line in lines: lineNumber += 1 line = line.strip(); # remove newline from the line print(f"{fileName} {lineNumber}: {line}") ``` ``` poe.txt 1: Once upon a midnight dreary, poe.txt 2: while I pondered, weak and weary, poe.txt 3: Over many a quaint and curious poe.txt 4: volume of forgotten lore— ``` # Writing files reverser.py ```python #!/usr/bin/python3 inFileName="poe.txt" outFileName="eop.txt" with open(inFileName) as inFile: with open(outFileName, 'w') as outFile: while True: line = inFile.readline() if not line: # If the line was empty, file end reached break; # so break the lop line = line.strip(); line = line[::-1] # Reverse the string line = line + "\n" outFile.write(line) ``` ```bash ./reverser.py cat eop.txt ,yraerd thgindim a nopu ecnO ,yraew dna kaew ,derednop I elihw suoiruc dna tniauq a ynam revO ``` # Writing CSV files ```python #!/usr/bin/python3 import csv ourResults = [] ourResults.append( ['Johnny', 'Cat', 4 ] ) ourResults.append( ['Viggo', 'Dog', 5 ] ) ourResults.append( ['Mat', 'Dog', 7 ] ) ourResults.append( ['Markus', 'Cat', 7 ] ) with open("out.csv", "w") as csvOutFile: writer = csv.writer(csvOutFile, dialect='excel') writer.writerow(['Name', 'Species', 'Age']) for row in ourResults: writer.writerow(row) ``` ```bash cat out.csv Name,Species,Age Johnny,Cat,4 Viggo,Dog,5 Mat,Dog,7 Markus,Cat,7 ``` # Writing CSV files ```python #!/usr/bin/python3 import csv csvHeader = [ 'Name', 'Species', 'Age' ] ourResults = [] ourResults.append( { 'Name': 'Johnny', 'Species': 'Cat', 'Age': 4 } ) ourResults.append( { 'Name': 'Viggo', 'Species': 'Dog', 'Age': 5 } ) ourResults.append( { 'Name': 'Mat', 'Species': 'Dog', 'Age': 7 } ) ourResults.append( { 'Name': 'Markus', 'Species': 'Cat', 'Age': 7 } ) with open("out.csv", "w") as csvOutFile: writer = csv.DictWriter(csvOutFile, fieldnames=csvHeader, dialect='excel') writer.writeheader() for row in ourResults: writer.writerow(row) ``` # String search ```python string.find(KEYWORD) #rfind is the same, but goes right to left # They return -1 if KEYWORD not found ``` ```python needle = "is" haystack = "This is a test" position = haystack.find(needle) # 2 position = haystack.rfind(needle) # 5 ``` # String slicing ```python string[ BEGIN : STEP : END ] ``` ```python # Slicing (extract substring) myString = "abcdefg" subString = myString[0:3:1] # First 3 characters (character 0,1,2) subString = myString[3:6:1] # From third character to sixth (def) substring = myString[-2::] # Last 2 characters subString = myString[::-1] # Step through string in reverse ``` # String splitting ```python string.split( DELIMITER ) string.split( DELIMITER, NUMBER_OF_SPLITS) # rsplit is the same, but goes right to left ``` ```python myString = "a,b,c,d,e,f,g" subString = myString.split(',') # ['a', 'b', 'c', 'd', 'e', 'f', 'g'] subString = myString.split(',', 1) # ['a', 'b,c,d,e,f,g'] subString = myString.rsplit(',', 1) # ['a,b,c,d,e,f', 'g'] ``` # Regular expressions ```python import re # regex module text = "He was carefully disguised but captured quickly by police." subString = re.split(r"but", text) # ['He was carefully disguised ', ' captured quickly by police.'] subString = re.findall(r"\w+ly\b", text) # ['carefully', 'quickly'] ``` More about regex at [https://docs.python.org/3/library/re.html](https://docs.python.org/3/library/re.html) # Searching in list ```python #!/usr/bin/python3 listOfAnimals = [ { "Name": "Viggo", "Species": "Dog", "Age": 5 }, { "Name": "Mat", "Species": "Dog", "Age": 6 }, { "Name": "Oliver", "Species": "Cat", "Age": 5 }, { "Name": "Luggie", "Species": "Dog", "Age": 5 } ] fiveYearOldDogs = [] for animal in listOfAnimals: if animal["Species"] == "Dog" and animal["Age"] == 5: fiveYearOldDogs.append(animal["Name"]) print(fiveYearOldDogs) ``` ``` ['Viggo', 'Luggie'] ``` # Functions ```python def FUN_NAME( ARGUMENTS ): BODY ``` # Functions ```python #!/usr/bin/python3 # repeater will return a textToRepeat repeated timesToRepeat times. # If timesToRepeat is not provided, a default value of 5 is used def repeater( textToRepeat, timesToRepeat=5 ): return textToRepeat * timesToRepeat repeatedString = repeater("test", 3) print(repeatedString) ``` ``` testtesttest ``` # Functions ```python #!/usr/bin/python3 listOfAnimals = [ { "Name": "Viggo", "Species": "Dog", "Age": 5 }, { "Name": "Mat", "Species": "Dog", "Age": 6 }, { "Name": "Oliver", "Species": "Cat", "Age": 5 }, { "Name": "Luggie", "Species": "Dog", "Age": 5 } ] def isLuggie( animal ): return animal["Species"] == "Dog" and animal["Age"] == 5 and animal["Name"] == "Luggie" num = 0 for animal in listOfAnimals: num += 1 if isLuggie(animal): print(f"Found {animal['Name']} he is number {num} in the list") ``` ``` Found Luggie he is number 4 in the list ``` # Sorting lists ```python list.sort() list.sort( reverse=True ) list.sort( key=FUNCTION ) ``` ```python #!/usr/bin/python3 myList = [ 2, 6, 1 ,4 ] myList.sort() # Sort will sort the list "in place" print(myList) myList = [ { 'Age': 5, 'Name': 'Viggo' }, { 'Age' : 2, 'Name': 'Mat' } ] def getAge(animal): return animal['Age'] myList.sort( key=getAge) print(myList) ``` ``` [1, 2, 4, 6] [{'Age': 2, 'Name': 'Mat'}, {'Age': 5, 'Name': 'Viggo'}] ``` # The end ![](./end.png)