Repository: exercism/python
Pull request #2406 (2021-04-26 03:58:32)

Loops spiffying
- Added an `about.md` for the `loops` concept.
- Added an `introduction.md` for the loops concept.
- Added `links.json` for the loops concept.
- Added `.meta/config.json` for loops concept.


Patch:
diff --git a/concepts/loops/.meta/config.json b/concepts/loops/.meta/config.json
index 9b9e8da5..c69c9321 100644
--- a/concepts/loops/.meta/config.json
+++ b/concepts/loops/.meta/config.json
@@ -1,5 +1,5 @@
 {
-  "blurb": "TODO: add blurb for this concept",
-  "authors": ["bethanyg", "cmccandless"],
+  "blurb": "There are two looping constructs:  `while` loops for _indefinite_ (uncounted) iteration and `for` loops for _definite_, (counted) iteration. The keywords `break`, `continue`, and `else` help customize loop behavior.",
+  "authors": ["BethanyG"],
   "contributors": []
 }
diff --git a/concepts/loops/about.md b/concepts/loops/about.md
index 79ac4c2a..9f62112c 100644
--- a/concepts/loops/about.md
+++ b/concepts/loops/about.md
@@ -1,2 +1,238 @@
 # About
 
+Python has two looping constructs.
+`while` loops for _indefinite_ (uncounted) iteration and `for` loops for _definite_, (counted) iteration.
+The keywords `break`, `continue`, and `else` help customize loop behavior.
+
+<br>
+
+## `While`
+
+[`while`][while statement] loops continue to exectute as long as the loop expression or "test" evaluates to `True` in a [`boolean context`][truth value testing], terminating when it evaluates to `False`.
+
+```python
+
+# Lists are considered "truthy" in a boolean context if they
+# contain one or more values, and "falsy" if they are empty.
+
+>>> placeholders = ["spam", "ham", "eggs", "green_spam", "green_ham", "green_eggs"]
+
+>>> while placeholders:
+...     print(placeholders.pop(0))
+...
+spam
+ham
+eggs
+green_spam
+green_ham
+green_eggs
+```
+
+<br>
+
+## `For`
+
+The basic [`for`][for statement] loop is better described as a _`for each`_ which cycles through the values of any [iterable object][iterable], terminating when there are no values returned from calling [`next()`][next built-in] (_raising a [`StopIteration`][stopiteration]_).
+
+```python
+
+>>> word_list = ["bird", "chicken", "barrel", "bongo"]
+
+>>> for word in word_list:
+...    if word.startswith("b"):
+...        print(f"{word.title()} starts with a B.")
+...    else:
+...        print(f"{word.title()} doesn't start with a B.")
+...
+Bird starts with a B.
+Chicken doesn't start with a B.
+Barrel starts with a B.
+Bongo starts with a B.
+
+```
+
+<br>
+
+## Sequence Object `range()`
+
+When there isn't a specific `iterable` given, the special [`range()`][range] sequence is used.  
+`range()` requires a number before which to `stop`, and can optionally take a `start` parameter.  
+If no `start` number is provided, the sequence will begin with 0.  
+`range()` objects are `lazy` (_values are generated on request_), support all [common sequence operations][common sequence operations], and take up a fixed amount of memory, no matter how long the sequence.
+Interestingly, `range()` [is not an iterator][range is not an iterator], and can be used many in non-looping contexts.
+
+```python
+# Here we use range to produce some numbers, rather than creating a list of them in memory.
+# The values will start with 1 and stop *before* 7
+
+>>> for number in range(1, 7):
+...    if number % 2 == 0:
+...       print(f"{number} is even.")
+...    else:
+...       print(f"{number} is odd.")
+1 is odd.
+2 is even.
+3 is odd.
+4 is even.
+5 is odd.
+6 is even.
+
+
+# range() can also take a *step* parameter.
+# Here we use range to produce only the "odd" numbers, starting with 3 and stopping *before* 15.
+
+>>> for number in range(3, 15, 2):
+...    if number % 2 == 0:
+...       print(f"{number} is even.")
+...    else:
+...       print(f"{number} is odd.")
+...
+3 is odd.
+5 is odd.
+7 is odd.
+9 is odd.
+11 is odd.
+13 is odd.
+
+```
+
+<br>
+
+## Values and Indexes with `enumerate()`
+
+If both values and indexes are needed, the built-in [`enumerate()`][enumerate] will return an [`iterator`][iterator] over (`index`, `value`) pairs:
+
+```python
+
+>>> word_list = ["bird", "chicken", "barrel", "apple"]
+
+# *index* and *word* are the loop variables.
+# Loop variables can be any valid python name.
+
+>>> for index, word in enumerate(word_list):
+...    if word.startswith("b"):
+...        print(f"{word.title()} (at index {index}) starts with a B.")
+...    else:
+...        print(f"{word.title()} (at index {index}) doesn't start with a B.")
+...
+Bird (at index 0) starts with a B.
+Chicken (at index 1) doesn't start with a B.
+Barrel (at index 2) starts with a B.
+Apple (at index 3) doesn't start with a B.
+
+
+# enumerate() can also be set to *start* the index count at a different number.
+# Here, the index count will start at 1.
+
+>>> for position, word in enumerate(word_list, start=1):
+...    if word.startswith("b"):
+...        print(f"{word.title()} (at position {position}) starts with a B.")
+...    else:
+...        print(f"{word.title()} (at position {position}) doesn't start with a B.")
+...
+Bird (at position 1) starts with a B.
+Chicken (at position 2) doesn't start with a B.
+Barrel (at position 3) starts with a B.
+Apple (at position 4) doesn't start with a B.
+
+```
+
+<br>
+
+## Altering Loop Behavior
+
+[`continue`][continue statement] can be used to skip forward to the next iteration.
+
+```python
+word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple", "bear"]
+
+# This will skip *bird*, at index 0
+for index, word in enumerate(word_list):
+    if index == 0:
+        continue
+    if word.startswith("b"):
+        print(f"{word.title()} (at index {index}) starts with a b.")
+
+Barrel (at index 2) starts with a b.
+Bongo (at index 3) starts with a b.
+Bear (at index 6) starts with a b.
+
+```
+
+[`break`][break statement] (_like in many C-related languages_) can be used to stop the iteration and "break out" of the innermost enclosing loop.
+
+```python
+>>>  word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple"]
+
+>>> for index, word in enumerate(word_list):
+...    if word.startswith("b"):
+...        print(f"{word.title()} (at index {index}) starts with a B.")
+...    elif word == 'sliver':
+...        break
+...    else:
+...        print(f"{word.title()} doesn't start with a B.")
+...print("loop broken.")
+...
+Bird (at index 0) starts with a B.
+Chicken doesn't start with a B.
+Barrel (at index 2) starts with a B.
+Bongo (at index 3) starts with a B.
+loop broken.
+
+```
+
+The loop [`else` clause][loop else] is unique to python, and can be used as a "wrap up" or "concluding" action for when iteration has been exhausted. `else` statements will not run if iteration terminates through a `break` statement.
+
+```python
+>>> word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple"]
+
+# Using enumerate to get both an index and a value.
+
+>>> for index, word in enumerate(word_list):
+...    word = word.title()
+...    if word.startswith("B"):
+...        print(f"{word} (at index {index}) starts with a B.")
+...# This executes once *StopIteration* is raised and there are no more items.
+...else:
+...    print(f"Found the above b-words, out of {len(word_list)} words in the word list.")
+...
+Bird (at index 0) starts with a B.
+Barrel (at index 2) starts with a B.
+Bongo (at index 3) starts with a B.
+Found the above b-words, out of 6 words in the word list.
+
+
+# Terminating a loop via *break* will bypass the loop *else* clause
+
+>>> for index, word in enumerate(word_list):
+...    word = word.title()
+...    if word.startswith("B"):
+...       print(f"{word} (at index {index}) starts with a B.")
+...    if word.startswith("S"):
+...        print("Found an S, stopping iteration.")
+...        break
+...# This is not run, because break was triggered
+... else:
+...    print(f"Found the above b-words, out of {len(word_list)} words in the word list.")
+...
+Bird (at index 0) starts with a B.
+Barrel (at index 2) starts with a B.
+Bongo (at index 3) starts with a B.
+Found an S, stopping iteration.
+
+```
+
+[next built-in]: https://docs.python.org/3/library/functions.html#next
+[stopiteration]: https://docs.python.org/3/library/exceptions.html#StopIteration
+[loop else]: https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops
+[for statement]: https://docs.python.org/3/reference/compound_stmts.html#for
+[range]: https://docs.python.org/3/library/stdtypes.html#range
+[break statement]: https://docs.python.org/3/reference/simple_stmts.html#the-break-statement
+[continue statement]: https://docs.python.org/3/reference/simple_stmts.html#the-continue-statement
+[while statement]: https://docs.python.org/3/reference/compound_stmts.html#the-while-statement
+[iterable]: https://docs.python.org/3/glossary.html#term-iterable
+[truth value testing]: https://docs.python.org/3/library/stdtypes.html#truth-value-testing
+[enumerate]: https://docs.python.org/3/library/functions.html#enumerate
+[iterator]: https://docs.python.org/3/glossary.html#term-iterator
+[common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
+[range is not an iterator]: https://treyhunner.com/2018/02/python-range-is-not-an-iterator/
diff --git a/concepts/loops/introduction.md b/concepts/loops/introduction.md
index 54fa5794..31798a33 100644
--- a/concepts/loops/introduction.md
+++ b/concepts/loops/introduction.md
@@ -1,88 +1,19 @@
 # Introduction
 
-TODO: the content below is copied from the exercise introduction and probably needs rewriting to a proper concept introduction
+There are two looping constructs.
+`while` loops for _indefinite_ (uncounted) iteration and `for` loops for  _definite_, (counted) iteration.
+The keywords `break`, `continue`, and `else` help customize loop behavior.
 
-## loops
+`while` loops continue to exectute as long as the loop expression or "test" evaluates to `True`, terminating when it evaluates to `False`.
 
-## Loops in Python
-
-There are 2 general ways in Python to loop through objects.
-
-- `while` loop (_indefinite_, or _uncounted_)
-- `for` loop (_definite_, or _counted_)
-
-## While loops
-
-While loops are _uncounted_ and based on a `conditional` statement for looping through objects.
-
-```
+```python
 while expression:
     set_of_statements
 ```
 
-When the statement evaluates to `True`, the loop advances and executes the code in the indented block - or "body" of the loop. Looping continues in this fashion until the conditional statement evaluates to `False`.
+`for` loops cycle through the values of any sequence, terminating when there are no more values returned.
 
 ```python
-i = 0
-while i < 3:
-    print(i)
-# => 0
-# => 1
-# => 2
-```
-
-## For Loops
-
-Unlike `while` loops, `for` loops are based on a counter. The Loop will execute until the counter/object being counted is exhausted. The counter in this case could be the indexes in a `list`, `string`, or `tuple` -- or the indexes in a `range()` object.
-
-```python
-for item in countable_object:
+for item in sequence:
     set_of_statements
 ```
-
-```python
->>> numbers = [1, 2, 3, 4, 5]
-
->>> for number in numbers:
-         print(number)
-#=> 1
-#=> 2
-#=> 3
-#=> 4
-#=> 5
-```
-
-## Breaking from loops
-
-Where you have a large set of objects that you want to loop through but would like to stop the loop execution when a certain condition is met, the `break` statement comes to the rescue.
-
-```python
-list_of_items = [1, 2, 3, 4, 5, 6, 7, 8]
-for item in list_of_items:
-    if item > 5:
-        break
-    print(item)
-# => 1
-# => 2
-# => 3
-# => 4
-# => 5
-```
-
-## Continuing in loops
-
-You will have important objects as well as non important ones. When you want to skip over the objects that you don't need to process you use the `continue` statement.
-
-Example: you want to process only odd numbers in a loop and not the event numbers.
-
-```python
-all_numbers = [1, 2, 3, 4, 5, 6, 7, 8]
-for num in all_numbers:
-    if num % 2 == 0:
-        continue
-    print(num)
-# => 1
-# => 3
-# => 5
-# => 7
-```
diff --git a/concepts/loops/links.json b/concepts/loops/links.json
index fe51488c..4a5c8454 100644
--- a/concepts/loops/links.json
+++ b/concepts/loops/links.json
@@ -1,1 +1,42 @@
-[]
+[
+  {
+    "url": "https://nedbatchelder.com/text/iter.html",
+    "description": "Ned Batchelder:  Loop Like a Native."
+  },
+  {
+    "url": "https://docs.python.org/3/tutorial/controlflow.html#for-statements",
+    "description": "Python Docs Tutorial: for statements"
+  },
+  {
+    "url": "https://realpython.com/python-for-loop/",
+    "description": "Real Python: For Loops"
+  },
+  {
+    "url": "https://realpython.com/python-while-loop/",
+    "description": "Real Python: While Loops"
+  },
+  {
+    "url": "https://realpython.com/python-enumerate/",
+    "description": "Real Python: enumerate()"
+  },
+  {
+    "url": "https://docs.python.org/3/tutorial/controlflow.html#the-range-function/",
+    "description": "Python Docs:  range() function."
+  },
+  {
+    "url": "https://realpython.com/python-for-loop/#the-range-function",
+    "description": "Real Python: range()"
+  },
+  {
+    "url": "https://treyhunner.com/2018/02/python-range-is-not-an-iterator/",
+    "description": "Trey Hunner:  range() is not an iterator"
+  },
+  {
+    "url": "https://docs.python.org/3/library/exceptions.html#StopIteration",
+    "description": "Python Docs:  StopIteration Error"
+  },
+  {
+    "url": "https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops",
+    "description": "Python Docs:  Control Flow for Loops"
+  }
+]
diff --git a/exercises/concept/making-the-grade/.docs/after.md b/exercises/concept/making-the-grade/.docs/after.md
deleted file mode 100644
index 09283c1d..00000000
--- a/exercises/concept/making-the-grade/.docs/after.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# After
-
-## Loops in Python
-
-There is always a scenario where you would need to go through a [`list` or `group`] of [`things` or `object` or `values`].
-When you have an `array` of objects or values to process, you need to `iterate` on the array and process them. You are going to do a repeated set of instructions on every single object or value in an array.
-
-There are 2 ways to loop through objects.
-
-- `while` loop
-- `for` loop
-
-## While loops
-
-While loops are based on `conditional` statement for looping through objects.
-
-```
-while expression:
-    set_of_statements
-```
-
-When the expression evaluates to `True` (truthiness) the loop will execute the set of statements till it reaches the end of the indent block. It will execute the loop again until the expression evaluates to `False`.
-
-```python
-i = 0
-while i < 3:
-    print(i)
-# => 0
-# => 1
-# => 2
-```
-
-## For Loops
-
-While loops are based on `Iterational` statement. The Loop will iterate over a `iterable` object.
-
-```
-for item in iterable_object:
-    set_of_statements
-```
-
-```python
-list_of_items = [1, 2, 3]
-for item in list_of_items:
-    print(item)
-# => 1
-# => 2
-# => 3
-```
-
-## Breaking from loops
-
-Where you have a large set of objects that you want to loop through and you dont want to go through all the objects in the loop but rather go through a certain number of desired objects and then stop the execution, then `break` comes to the rescue.
-
-When you want to break from an iterable loop on any condition, you can use the `break` statement
-
-```python
-list_of_items = [1, 2, 3, 4, 5, 6, 7, 8]
-for item in list_of_items:
-    if item > 5:
-        break
-    print(item)
-# => 1
-# => 2
-# => 3
-# => 4
-# => 5
-```
-
-## Continuing in loops
-
-You will have important objects as well as non important ones. When you want to skip over the objects that you dont need to process you use the continue statement.
-
-Example: you want to process only odd numbers in a loop and not the event numbers.
-
-```python
-all_numbers = [1, 2, 3, 4, 5, 6, 7, 8]
-for num in all_numbers:
-    if num % 2 == 0:
-        continue
-    print(num)
-# => 1
-# => 3
-# => 5
-# => 7
-```
-
-## Else in Loops
-
-An `else` clause in a loop will get executed when there is no `break` that happebed while you were processing your loop.
-This gets useful if you want to go over fail over scenarios.
-
-```python
-x = None
-for i in list_of_items:
-    if i > 10:
-        x = i
-        break
-else:
-    x = 10
-```
-
-What the above code does is it sets the variable x to 10 if the loop inside was not able to find an item that is more than 10.
diff --git a/exercises/concept/making-the-grade/.docs/hints.md b/exercises/concept/making-the-grade/.docs/hints.md
index 33e7d14d..b229dd4d 100644
--- a/exercises/concept/making-the-grade/.docs/hints.md
+++ b/exercises/concept/making-the-grade/.docs/hints.md
@@ -2,22 +2,60 @@
 
 ## General
 
-- Make sure you are comfortable with using the `in` concept while looping through in for loops.
+- `while` loops are used for _indefinite_ (uncounted) iteration
+- `for` loops are used for _definite_, (counted) iteration.
+- The keywords `break` and `continue` help customize loop behavior.
+- `range(<start>, stop, <step>)` can be used to generate a sequence for a loop counter.
+- The bulit-in `enumerate()` will return (`<value>`, `<index>`) pairs to iterate over.
 
-You are a teacher and you are correcting papers of your students who wrote an exam you just conducted. You are tasked with the following activities.
+Also being familure with the following can help with completing the tasks:
 
-## 1. Failed Students
+- [`lists`][list]: indexing, nested lists, [`<list>.append`][append and pop], [`<list>.pop()`][append and pop].
+- [`str`][str]: `str()` constructor, using the `+` to concatenate strings, optionally, [`f-strings`][f-strings].
 
-- Iterate through the marks one by one. For each loop check if the mark is less than or equal to 40. If so, then increment a count and after iterating through all marks in the loop return the count.
+## 1. Rounding Scores
 
-## 2. Top Marks
+- `While` loops will continue to execute until their condition evaluates to `False`.
+- <list>.pop() will remove and return the last item in a list.
+- Empty `lists` evaluae to `False` (most empty objects in Python are "Falsy")
+-
 
-- Iterate through the list of marks one by one. If you find a mark which is more than x, just continue and go to the next mark.
+## 2. Non-Passing Students
 
-## 3. First K Students.
+- There's no need to declare loop counters or index counters when iterating through an object using a `for` loop.
+- A results counter does need to set up and _incremented_ -- you'll want to `return` it when the loop terminates.
+-
 
-- Start an index variable at 0 and loop through the elements till we reach the index variable equals to k. once that happens you can return the marks.
+## 3. The "Best"
 
-## 4. Full Marks
+- There's no need to declare loop counters or index counters when iterating through an object using a `for` loop.
+- Having an emptey list to add the "best" marks to is helpful here.
+- `<list>.append()` can help add things to the results list.
+-
 
-- There may be or may not be a student with full marks - 100. Either way you have to loop through all the marks and find whether we have a student with full marks else just return No Hundreds
+## 4. Calculating Letter Grades
+
+- These are _lower thresholds_.  The _lower threshold_ for a "D" is a score of **41**, since an "F" is **<= 40**.
+- [`range()`][range] can be helpful here to generate a sequence with the proper "F" -> "A" increments.
+- [`round()`][round] without parameters should round off increments nicely.
+- As with "the best" task, `<list>.append()` could be useful here to append items from `range()` into a results list.
+
+## 5. Matching Names to Scores
+
+- [`enumerate()`][enumerate] could be helpful here.
+- If both lists are the same length and sorted the same way, could you use the index from one to retrieve a value from the other?
+-
+
+## 6. A "Perfect" Score
+
+- There may be or may not be a student with a score of 100, and you can't return "No perfect score." without checking all the scores.
+- The [`control flow`][control flow] satements `continue` and `break`break may be useful here to move past unwanted values.
+
+[list]: https://docs.python.org/3/library/stdtypes.html#list
+[str]: https://docs.python.org/3/library/stdtypes.html#str
+[f-strings]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals
+[append and pop]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists
+[enumerate]: https://docs.python.org/3/library/functions.html#enumerate
+[control flow]: https://docs.python.org/3/tutorial/controlflo.html#break-and-continue-statements-and-else-clauses-on-loops
+[range]: https://docs.python.org/3/tutorial/controlflow.html#the-range-function
+[round]: https://docs.python.org/3/library/functions.html#round
diff --git a/exercises/concept/making-the-grade/.docs/instructions.md b/exercises/concept/making-the-grade/.docs/instructions.md
index a9384af5..9c7b9579 100644
--- a/exercises/concept/making-the-grade/.docs/instructions.md
+++ b/exercises/concept/making-the-grade/.docs/instructions.md
@@ -1,64 +1,97 @@
 # Instructions
 
-You are a teacher and you are correcting papers of your students who wrote an exam you just conducted. You are tasked with the following activities.
+You're a teaching assistant correcting student exams.  
+Keeping track of results manually is getting both tedious and mistake-prone.  
+You decide to make things a little more interesting by putting together some functions to count and calculate results for the class.
 
-## 1. Failed Students
+## 1. Rounding Scores
 
-Create the function `count_failed_students()` that takes one parameter: `student_marks`. This function should count the number of students who have failed the subject, returning the count as an integer. We say that a student has failed if their mark is less than or equal to **40**.
+While you can give "partial credit" on exam questions, overall exam scores have to be `int`s.  
+So before you can do anything else with the class scores, you need to go through the grades and turn any `float` scores into `ints`.  Lucky for you, Python has the built-in [`round()`][round] function you can use.
 
-Note: `Iterate` through the student marks to find out your answer.
-​
-Result should be an `int`.
+A score of 75.45 or 75.49 will round to 75.  A score of 43.50 or 43.59 will round to 44.  
+There shouldn't be any scores that have more than two places after the decimal point.
+
+Create the function `round_scores()` that takes a `list` of `student_scores`.  
+This function should _consume_ the input `list` and `return` a new list with all the scores converted to `int`s.
+
+```python
+>>> student_scores = [90.33, 40.5, 55.44, 70.05, 30.55, 25.45, 80.45, 95.3, 38.7, 40.3]
+>>> round_scores(student_scores)
+...
+[40, 39, 95, 80, 25, 31, 70, 55, 40, 90]
+```
+
+## 2. Non-Passing Students
+
+As you were grading the exam, you noticed some students weren't performing as well as you'd hoped.  
+But you were distracted, and forgot to note exactly _how many_ students.
+
+Create the function `count_failed_students()` that takes a `list` of `student_scores`. This function should count up the number of students who don't have passing scores and return that count as an integer. A student needs a score greater than **40** to achive a passing grade on the exam.
 
 ```python
->>> count_failed_students(student_marks=[90,40,55,70,30]))
-2
+>>> count_failed_students(student_scores=[90,40,55,70,30,25,80,95,38,40])
+5
 ```
 
-## 2. Top Marks
+## 3. The "Best"
+
+The teacher you're assisting wants to find the group of students who've performed "the best" on this exam. What qualifies as "the best" fluctuates, so you'll need to find the student scores that are **greater than or equal to** the current threshold.
 
-The Headmaster wants to find the group of top students. What qualifies student marks as `top marks` fluctuates, and you will need to find the student marks that are **greater than or equal to** the current threshold.
+Create the function `above_threshold()` taking `student_scores` (a list of grades), and `threshold` (the "top score" threshold) as parameters.  This function should return a `list` of all scores that are `>=` to `threshold`.
 
-Create the function `above_threshold()` where `student_marks` and `threshold` are the two required parameters:
+```python
+>>> above_threshold(student_scores=[90,40,55,70,30,68,70,75,83,96], threshold=75)
+[90,75,83,96]
+```
 
-1. `student_marks` are a list of marks for each student
-2. `threshold` is the top mark threshold. Marks greater than or equal to this number are considered "top marks" .
+## 4. Calculating Letter Grades
 
-This function should return a `list` of all marks that are greater than or equal to a scoring threshold.
+The teacher you're assisting likes to assign letter grades as well as mumeric scores.  
+Since students rarely score 100 on an exam, the "letter grade" lower thresholds are calculated based on the highest score achieved, and increment evenly between the high score and the failing threshold of **<= 40**.
 
-**Note:** If you find a mark which is less than the threshold, you should `continue` to evaluating the next mark.​
+Create the function `letter_grades()` that takes the `highest` score on the exam as a parameter, and returns a `list` of lower score thresholds for each letter grade from "F" to "A".
 
 ```python
->>> above_threshold(student_marks=[90,40,55,70,30], 70)
-[90, 70]
+>>> letter_grades(highest=100)
+[41, 56, 71, 86]
+
+>>> letter_grades(highest=88)
+[41, 53, 65, 77]
 ```
 
-## 3. First K Students.
 
-Create the function `first_k_student_marks()` with parameters `student_marks` and `k`
+## 5. Matching Names to Scores
+
+You have a list of exam scores in descending order, and another list of student names also sorted in descending order by their exam scores.  You'd like to match each student name with their exam score, and print out an overall class ranking.
 
-1. Student marks are a list of marks of each student
-2. k is the number of students.
+Create the function `student_ranking()` with parameters `student_scores` and `student_names`.  Match each student name on the `student_names` list with their score from the `student_scores` list.  You can assume each argument `list` will be sorted from highest score(er) to lowest score(er).  The function should return a `list` of strings with the format "<rank>. <student name> : <student score>".
 
-You need to return the first K number of student Marks. Once you reach K number of students, you should exit out of the loop.
 
 ```python
->>> first_k_student_marks(student_marks=[90,80,100], k=1)
-[90]
+>>> student_scores = [100, 99, 90, 84, 66, 53, 47]
+>>> student_names =  ['Joci', 'Sara','Kora','Jan','John','Bern', 'Fred']
+>>> student_ranking(student_scores, student_names)
+...
+['1. Joci: 100', '2. Sara: 99', '3. Kora: 90', '4. Jan: 84', '5. John: 66', '6. Bern: 53', '7. Fred: 47']
 ```
 
-## 4. Full Marks
+## 6. A "Perfect" Score
 
-Create the function `perfect_score()` with parameter `student_info`.
-`student_info` is a dictionary containing the names and marks of the students `{"Charles": 90, "Tony": 80}`
+Although a "perfect" score of 100 is rare on an exam, it is interesting to know if at least one student has achieved it.
 
-Find if we have any students who scored "full marks"(_100%_)on the exam. If we don't find any "full marks" in `student_info`, we should return "No hundreds"
+Create the function `perfect_score()` with parameter `student_info`.
+`student_info` is a `list` of lists containing the name and score of each student: `[["Charles", 90], ["Tony", 80]]`.
+The function should `return` _the first_ [<name>, <score>] pair of the student who scored 100 on the exam.
 
-Return the first student who has scored full marks - 100.
+ If no 100 scores are found in `student_info`, "No perfect scores." should be returned.
 
 ```python
->>> perfect_score(student_info={"Charles": 90, "Tony": 80, "Alex":100})
-Alex
->>> perfect_score(student_info={"Charles": 90, "Tony": 80})
-No hundreds
+>>> perfect_score(student_info=[["Charles", 90], ["Tony", 80], ["Alex", 100]])
+["Alex", 100]
+
+>>> perfect_score(student_info=[["Charles", 90], ["Tony", 80]])
+"No perfect scores."
 ```
+
+[round]: https://docs.python.org/3/library/functions.html#round
diff --git a/exercises/concept/making-the-grade/.docs/introduction.md b/exercises/concept/making-the-grade/.docs/introduction.md
index ab71d6a3..dc42adbc 100644
--- a/exercises/concept/making-the-grade/.docs/introduction.md
+++ b/exercises/concept/making-the-grade/.docs/introduction.md
@@ -1,84 +1,192 @@
 # Introduction
 
-## Loops in Python
+Python has two looping constructs.
+`while` loops for _indefinite_ (uncounted) iteration and `for` loops for _definite_, (counted) iteration.
+The keywords `break`, `continue`, and `else` help customize loop behavior.
 
-There are 2 general ways in Python to loop through objects.
+<br>
 
-- `while` loop (_indefinite_, or _uncounted_)
-- `for` loop (_definite_, or _counted_)
+## While
 
-## While loops
+[`while`][while statement] loops continue to exectute as long as the loop expression or "test" evaluates to `True` in a [`boolean context`][truth value testing], terminating when it evaluates to `False`.
 
-While loops are _uncounted_ and based on a `conditional` statement for looping through objects.
+```python
 
-```
-while expression:
-    set_of_statements
-```
+# Lists are considered "truthy" in a boolean context if they
+# contain one or more values, and "falsy" if they are empty.
 
-When the statement evaluates to `True`, the loop advances and executes the code in the indented block - or "body" of the loop. Looping continues in this fashion until the conditional statement evaluates to `False`.
+>>> placeholders = ["spam", "ham", "eggs", "green_spam", "green_ham", "green_eggs"]
 
-```python
-i = 0
-while i < 3:
-    print(i)
-# => 0
-# => 1
-# => 2
+>>> while placeholders:
+...     print(placeholders.pop(0))
+...
+spam
+ham
+eggs
+green_spam
+green_ham
+green_eggs
 ```
 
-## For Loops
+<br>
 
-Unlike `while` loops, `for` loops are based on a counter. The Loop will execute until the counter/object being counted is exhausted. The counter in this case could be the indexes in a `list`, `string`, or `tuple` -- or the indexes in a `range()` object.
+## For
+
+The basic [`for`][for statement] loop is better described as a _`for each`_ which cycles through the values of any [iterable object][iterable], terminating when there are no values returned from calling [`next()`][next built-in].
 
 ```python
-for item in countable_object:
-    set_of_statements
+
+>>> word_list = ["bird", "chicken", "barrel", "bongo"]
+
+>>> for word in word_list:
+...    if word.startswith("b"):
+...        print(f"{word.title()} starts with a B.")
+...    else:
+...        print(f"{word.title()} doesn't start with a B.")
+...
+Bird starts with a B.
+Chicken doesn't start with a B.
+Barrel starts with a B.
+Bongo starts with a B.
+
 ```
 
+<br>
+
+## Sequence Object range()
+
+When there isn't a specific `iterable` given, the special [`range()`][range] sequence is used.  
+`range()` requires an `int` before which to `stop`, and can optionally take `start` and `step` parameters.  
+If no start `int` is provided, the sequence will begin with 0.  
+`range()` objects are `lazy` (_values are generated on request_), support all [common sequence operations][common sequence operations], and take up a fixed amount of memory, no matter how long the sequence.
+
 ```python
->>> numbers = [1, 2, 3, 4, 5]
-
->>> for number in numbers:
-         print(number)
-#=> 1
-#=> 2
-#=> 3
-#=> 4
-#=> 5
+# Here we use range to produce some numbers, rather than creating a list of them in memory.
+# The values will start with 1 and stop *before* 7
+
+>>> for number in range(1, 7):
+...    if number % 2 == 0:
+...       print(f"{number} is even.")
+...    else:
+...       print(f"{number} is odd.")
+1 is odd.
+2 is even.
+3 is odd.
+4 is even.
+5 is odd.
+6 is even.
+
+# range() can also take a *step* parameter.
+# Here we use range to produce only the "odd" numbers, starting with 3 and stopping *before* 15.
+
+>>> for number in range(3, 15, 2):
+...    if number % 2 == 0:
+...       print(f"{number} is even.")
+...    else:
+...       print(f"{number} is odd.")
+...
+3 is odd.
+5 is odd.
+7 is odd.
+9 is odd.
+11 is odd.
+13 is odd.
+
 ```
 
-## Breaking from loops
+<br>
 
-Where you have a large set of objects that you want to loop through but would like to stop the loop execution when a certain condition is met, the `break` statement comes to the rescue.
+## Values and Indexes with enumerate()
+
+If both values and indexes are needed, the built-in [`enumerate()`][enumerate] will return (`index`, `value`) pairs:
 
 ```python
-list_of_items = [1, 2, 3, 4, 5, 6, 7, 8]
-for item in list_of_items:
-    if item > 5:
-        break
-    print(item)
-# => 1
-# => 2
-# => 3
-# => 4
-# => 5
+
+>>> word_list = ["bird", "chicken", "barrel", "apple"]
+
+# *index* and *word* are the loop variables.
+# Loop variables can be any valid python name.
+
+>>> for index, word in enumerate(word_list):
+...    if word.startswith("b"):
+...        print(f"{word.title()} (at index {index}) starts with a B.")
+...    else:
+...        print(f"{word.title()} (at index {index}) doesn't start with a B.")
+...
+Bird (at index 0) starts with a B.
+Chicken (at index 1) doesn't start with a B.
+Barrel (at index 2) starts with a B.
+Apple (at index 3) doesn't start with a B.
+
+
+
+# The same method can be used as a "lookup" for pairing items between two lists.
+# Of course, if the lengths or indexs don't line up, this doesn't work.
+
+>>> word_list = ["cat", "chicken", "barrel", "apple", "spinach"]
+>>> category_list = ["mammal", "bird", "thing", "fruit", "vegetable"]
+
+>>> for index, word in enumerate(word_list):
+...    print(f"{word.title()} is in category: {category_list[index]}")
+...
+Cat is in category: mammal
+Chicken is in category: bird
+Barrel is in category: thing
+Apple is in category: fruit
+Spinach is in category: vegetable
 ```
 
-## Continuing in loops
+<br>
 
-You will have important objects as well as non important ones. When you want to skip over the objects that you don't need to process you use the `continue` statement.
+## Altering Loop Behavior
 
-Example: you want to process only odd numbers in a loop and not the event numbers.
+[`continue`][continue statement] can be used to skip forward to the next iteration.
 
 ```python
-all_numbers = [1, 2, 3, 4, 5, 6, 7, 8]
-for num in all_numbers:
-    if num % 2 == 0:
+word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple", "bear"]
+
+# This will skip *bird*, at index 0
+for index, word in enumerate(word_list):
+    if index == 0:
         continue
-    print(num)
-# => 1
-# => 3
-# => 5
-# => 7
+    if word.startswith("b"):
+        print(f"{word.title()} (at index {index}) starts with a b.")
+
+Barrel (at index 2) starts with a b.
+Bongo (at index 3) starts with a b.
+Bear (at index 6) starts with a b.
+
 ```
+
+[`break`][break statement] (_like in many C-related languages_) can be used to stop the iteration and "break out" of the innermost enclosing loop.
+
+```python
+>>>  word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple"]
+
+>>> for index, word in enumerate(word_list):
+...    if word.startswith("b"):
+...        print(f"{word.title()} (at index {index}) starts with a B.")
+...    elif word == 'sliver':
+...        break
+...    else:
+...        print(f"{word.title()} doesn't start with a B.")
+...print("loop broken.")
+...
+Bird (at index 0) starts with a B.
+Chicken doesn't start with a B.
+Barrel (at index 2) starts with a B.
+Bongo (at index 3) starts with a B.
+loop broken.
+
+```
+
+[for statement]: https://docs.python.org/3/reference/compound_stmts.html#for
+[range]: https://docs.python.org/3/library/stdtypes.html#range
+[break statement]: https://docs.python.org/3/reference/simple_stmts.html#the-break-statement
+[continue statement]: https://docs.python.org/3/reference/simple_stmts.html#the-continue-statement
+[while statement]: https://docs.python.org/3/reference/compound_stmts.html#the-while-statement
+[iterable]: https://docs.python.org/3/glossary.html#term-iterable
+[truth value testing]: https://docs.python.org/3/library/stdtypes.html#truth-value-testing
+[enumerate]: https://docs.python.org/3/library/functions.html#enumerate
+[common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
+[next built-in]: https://docs.python.org/3/library/functions.html#next
diff --git a/exercises/concept/making-the-grade/.meta/config.json b/exercises/concept/making-the-grade/.meta/config.json
index d6503ffb..1277447f 100644
--- a/exercises/concept/making-the-grade/.meta/config.json
+++ b/exercises/concept/making-the-grade/.meta/config.json
@@ -1,18 +1,9 @@
 {
   "blurb": "Learn about loops by grading and organzing your students exam scores.",
-  "authors": [
-    "mohanrajanr",
-    "BethanyG"
-  ],
+  "authors": ["mohanrajanr", "BethanyG"],
   "files": {
-    "solution": [
-      "loops.py"
-    ],
-    "test": [
-      "loops_test.py"
-    ],
-    "exemplar": [
-      ".meta/exemplar.py"
-    ]
+    "solution": ["loops.py"],
+    "test": ["loops_test.py"],
+    "exemplar": [".meta/exemplar.py"]
   }
 }
diff --git a/exercises/concept/making-the-grade/.meta/exemplar.py b/exercises/concept/making-the-grade/.meta/exemplar.py
index 6010f79e..99a3dd6e 100644
--- a/exercises/concept/making-the-grade/.meta/exemplar.py
+++ b/exercises/concept/making-the-grade/.meta/exemplar.py
@@ -1,28 +1,44 @@
-def count_failed_students(student_marks):
-    failed_marks = 0
-    for mark in student_marks:
-        if mark <=40:
-            failed_marks += 1
-    return failed_marks
+def round_scores(student_scores):
+    rounded = []
+    while student_scores:
+        rounded.append(round(student_scores.pop()))
+    return rounded
 
-def above_threshold(student_marks, x):
-    above_marks = []
-    for mark in student_marks:
-        if mark >= x:
-            above_marks.append(mark)
-    return above_marks
+def count_failed_students(student_scores):
+    non_passing = 0
+    for score in student_scores:
+        if score <= 40:
+            non_passing += 1
+    return non_passing
 
-def first_k_student_marks(student_marks, k):
-    i = 0
-    marks = []
-    while i < k:
-        marks.append(student_marks[i])
-        i += 1
-    return marks
+def above_threshold(student_scores, threshold):
+    above_threshold = []
+    for score in student_scores:
+        if score >= threshold:
+            above_threshold.append(score)
+    return above_threshold
+
+def letter_grades(highest):
+    increment = round((highest - 40)/4)
+    scores = []
+    for score in range(41, highest, increment):
+        scores.append(score)
+    return scores
+
+def student_ranking(student_scores, student_names):
+    results = []
+    for index, name in enumerate(student_names):
+        rank_string = str(index + 1) + ". " + name + ": " + str(student_scores[index])
+        results.append(rank_string)
+        #results.append(f"{index + 1}. {name}: {student_scores[index]}")
+    return results
 
 def perfect_score(student_info):
-    for name, mark in student_info.items():
-        if mark == 100:
-            return name
-    else:
-        return "No Centums"
+    result = "No perfect score."
+    for item in student_info:
+        if item[1] == 100:
+            result = item
+            break
+        else:
+            continue
+    return result
diff --git a/exercises/concept/making-the-grade/loops.py b/exercises/concept/making-the-grade/loops.py
index d18c7871..c3d632e7 100644
--- a/exercises/concept/making-the-grade/loops.py
+++ b/exercises/concept/making-the-grade/loops.py
@@ -1,11 +1,42 @@
-def count_failed_students(student_marks):
+def round_scores(student_scores):
+    '''
+    :param student_scores: list of student exame scores as float or int.
+    :return: list of student scores *rounded* to nearest integer value.
+    '''
     pass
 
-def above_threshold(student_marks, x):
+def count_failed_students(student_scores):
+    '''
+    :param student_scores: list of integer studnet scores.
+    :return: integer count of student scores at or below 40.
+    '''
     pass
 
-def first_k_student_marks(student_marks, k):
+def above_threshold(student_scores, threshold):
+    '''
+    :param student_scores: list of integer scores
+    :param threshold :  integer
+    :return: list of integer scores that are at or above the "best" threshold.
+    '''
     pass
 
+def letter_grades(highest):
+    '''
+    :param highes: integer of higest exam score.
+    :return: list of integer score thresholds for each F-A letter grades.
+    '''
+    pass
+def student_ranking(student_scores, student_names):
+    '''
+     :param student_scores: list of scores in descending order.
+     :param student_names: list of names in descending order by exam score.
+     :return: list of strings in format ["<rank>. <student name>: <score>"].
+     '''
+    pass
 def perfect_score(student_info):
+    '''
+    :param student_info: list of [<student name>, <score>] lists
+    :return: First [<student name>, 100] found OR "No perfect score."
+    '''
     pass
+


Test patch:
diff --git a/exercises/concept/making-the-grade/loops_test.py b/exercises/concept/making-the-grade/loops_test.py
index 5b2598cf..e9d98177 100644
--- a/exercises/concept/making-the-grade/loops_test.py
+++ b/exercises/concept/making-the-grade/loops_test.py
@@ -1,61 +1,138 @@
 import unittest
-from loops import *
-
-
-class TestLoops(unittest.TestCase):
-    
-    def test_count_failed_students_1(self):
-        self.assertEqual(
-            count_failed_students(student_marks=[40,40,35,70,30]),
-            4,
-            msg="Number of failed students is invalid",
-        )
-
-    def test_count_failed_students_2(self):
-        self.assertEqual(
-            count_failed_students(student_marks=[90,80,55,70,65]),
-            0,
-            msg="Number of failed students is invalid",
-        )
-
-    def test_above_threshold_1(self):
-        self.assertEqual(
-            above_threshold(student_marks=[90,40,55,70,30], x=100),
+from loops import (
+    round_scores,
+    count_failed_students,
+    above_threshold,
+    letter_grades,
+    student_ranking,
+    perfect_score,
+)
+
+
+class MakingTheGradeTest(unittest.TestCase):
+
+    def test_round_scores(self):
+        input_data = [
+            [90.33, 40.5, 55.44, 70.05, 30.55, 25.45, 80.45, 95.3, 38.7, 40.3],
             [],
-            msg="The Number of marks above given marks is incorrect",
-        )
-
-    def test_above_threshold_2(self):
-        self.assertEqual(
-            above_threshold(student_marks=[90,40,55,70,30], x=70),
-            [90, 70],
-            msg="The Number of marks above given marks is incorrect",
-        )
-
-    def test_first_k_student_marks_1(self):
-        self.assertEqual(
-            first_k_student_marks(student_marks=[90,80,100,70], k=4),
-            [90,80,100,70],
-            msg="The Number of First K Students are incorrect",
-        )
-
-    def test_first_k_student_marks_2(self):
-        self.assertEqual(
-            first_k_student_marks(student_marks=[90,80,100], k=1),
-            [90],
-            msg="The Number of First K Students are incorrect",
-        )
-
-    def test_full_mark_student_1(self):
-        self.assertEqual(
-            perfect_score(student_info={"Charles": 90, "Tony": 80, "Thor": 60}),
-            "No Centums",
-            msg="Centum Scorer name is wrong",
-        )
-
-    def test_full_mark_student_2(self):
-        self.assertEqual(
-            perfect_score(student_info={"Charles": 90, "Tony": 80, "Mark": 100}),
-            "Mark",
-            msg="Centum Scorer name is wrong",
-        )
+            [50, 36.03, 76.92, 40.7, 43, 78.29, 63.58, 91, 28.6, 88.0],
+            [.5],
+            [1.5]
+        ]
+        result_data = [
+            [40, 39, 95, 80, 25, 31, 70, 55, 40, 90],
+            [],
+            [88, 29, 91, 64, 78, 43, 41, 77, 36, 50],
+            [0],
+            [2]
+        ]
+        number_of_variants = range(1, len(input_data) + 1)
+
+        for variant, scores, results in zip(number_of_variants, input_data, result_data):
+            with self.subTest(f"variation #{variant}", scores=scores, results=results):
+                self.assertEqual(round_scores(scores), results,
+                                 msg=f'Expected: {results} but one or more {scores} were rounded incorrectly.')
+
+
+    def test_no_failed_students(self):
+        scores = [89, 85, 42, 57, 90, 100, 95, 48, 70, 96]
+        expected = 0
+        self.assertEqual(count_failed_students(scores), expected,
+        msg=f"Expected the count to be {expected}, but the count wasn't calculated correctly.")
+
+    def test_some_failed_students(self):
+        scores = [40, 40, 35, 70, 30, 41, 90]
+        expected = 4
+        self.assertEqual(count_failed_students(scores), expected,
+            msg=f"Expected the count to be {expected}, but the count wasn't calculated correctly.")
+
+    def test_above_threshold(self):
+        input_data = [
+            [40, 39, 95, 80, 25, 31, 70, 55, 40, 90],
+            [88, 29, 91, 64, 78, 43, 41, 77, 36, 50],
+            [100, 89],
+            [88, 29, 91, 64, 78, 43, 41, 77, 36, 50],
+            []
+        ]
+        thresholds = [98, 80, 100, 78, 80]
+        result_data = [
+            [],
+            [88, 91],
+            [100],
+            [88, 91, 78],
+            []
+        ]
+        number_of_variants = range(1, len(input_data) + 1)
+
+        for variant, score, threshold, result in zip(number_of_variants, input_data, thresholds, result_data):
+            with self.subTest(f"variation #{variant}", score=score, threshold=threshold, result=result):
+                self.assertEqual(above_threshold(score, threshold), result,
+                                 msg=f'Expected: {result} but the number of scores above {threshold} is incorrect.')
+
+    def test_letter_grades(self):
+        input_data = [100, 97, 85, 92, 81]
+        result_data = [
+            [41, 56, 71, 86],
+            [41, 55, 69, 83],
+            [41, 52, 63, 74],
+            [41, 54, 67, 80],
+            [41, 51, 61, 71]
+        ]
+        number_of_variants = range(1, len(input_data) + 1)
+
+        for variant, highest, result in zip(number_of_variants, input_data, result_data):
+            with self.subTest(f"variation #{variant}", highest=highest, result=result):
+                self.assertEqual(letter_grades(highest), result,
+                                 msg=f'Expected: {result} but the grade thresholds for a high score of {highest} are incorrect.')
+
+
+    def test_student_ranking(self):
+        scores = [
+            [100, 98, 92, 86, 70, 68, 67, 60, 50],
+            [82],
+            [88, 73],
+        ]
+        names = [
+            ['Rui', 'Betty', 'Joci', 'Yoshi', 'Kora', 'Bern', 'Jan', 'Rose'],
+            ['Betty'],
+            ['Paul', 'Ernest'],
+        ]
+        result_data = [
+            ['1. Rui: 100', '2. Betty: 98', '3. Joci: 92', '4. Yoshi: 86',
+             '5. Kora: 70', '6. Bern: 68', '7. Jan: 67', '8. Rose: 60'],
+            ['1. Betty: 82'],
+            ['1. Paul: 88', '2. Ernest: 73']
+        ]
+        number_of_variants = range(1, len(scores) + 1)
+
+        for variant, scores, names, results in zip(number_of_variants, scores, names, result_data):
+            with self.subTest(f"variation #{variant}", scores=scores, names=names, results=results):\
+                self.assertEqual(student_ranking(scores, names), results,
+                                 msg=f'Expected: {results} but the rankings were compiled incorrectly.')
+
+    def test_perfect_score(self):
+       input_data =  [
+                        [['Rui', 60],['Joci', 58],['Sara', 91],['Kora', 93], ['Alex', 42],
+                         ['Jan', 81],['Lilliana', 40],['John', 60],['Bern', 28],['Vlad', 55]],
+
+                        [['Yoshi', 52],['Jan', 86], ['Raiana', 100], ['Betty', 60],
+                         ['Joci', 100],['Kora', 81],['Bern', 41], ['Rose', 94]],
+
+                        [['Joci', 100],['Vlad', 100],['Raiana', 100],['Alessandro', 100]],
+                        [['Jill', 30], ['Paul', 73],],
+                        []
+                    ]
+       result_data = [
+           "No perfect score.",
+           ['Raiana', 100],
+           ['Joci', 100],
+           "No perfect score.",
+           "No perfect score."
+       ]
+
+       number_of_variants = range(1, len(input_data) + 1)
+
+       for variant, scores, results in zip(number_of_variants, input_data, result_data):
+           with self.subTest(f"variation #{variant}", scores=scores, results=results):
+               self.assertEqual(perfect_score(scores), results,
+                                msg=f'Expected: {results} but got something different for perfect scores.')
\ No newline at end of file