Repository: exercism/python
Pull request #3279 (2022-12-10 22:42:30)

[Exercise]: added square-root


Patch:
diff --git a/config.json b/config.json
index f267cbd8..d1399f2f 100644
--- a/config.json
+++ b/config.json
@@ -593,6 +593,19 @@
         ],
         "difficulty": 2
       },
+      {
+        "slug": "square-root",
+        "name": "Square Root",
+        "uuid": "c32f994a-1080-4f05-bf88-051975a75d64",
+        "practices": ["number"],
+        "prerequisites": [
+          "basics",
+          "number",
+          "conditionals",
+          "loops"
+        ],
+        "difficulty": 2
+      },
       {
         "slug": "scrabble-score",
         "name": "Scrabble Score",
@@ -1271,6 +1284,19 @@
         ],
         "difficulty": 3
       },
+      {
+        "slug": "pascals-triangle",
+        "name": "Pascals Triangle",
+        "uuid": "e1e1c7d7-c1d9-4027-b90d-fad573182419",
+        "practices": ["recursion"],
+        "prerequisites": [
+          "basics",
+          "conditionals",
+          "lists",
+          "numbers"
+        ],
+        "difficulty": 4
+      },
       {
         "slug": "grep",
         "name": "Grep",
@@ -2140,15 +2166,6 @@
         "difficulty": 3,
         "status": "deprecated"
       },
-      {
-        "slug": "pascals-triangle",
-        "name": "Pascal's Triangle",
-        "uuid": "e1e1c7d7-c1d9-4027-b90d-fad573182419",
-        "practices": [],
-        "prerequisites": [],
-        "difficulty": 3,
-        "status": "deprecated"
-      },
       {
         "slug": "point-mutations",
         "name": "Point Mutations",
diff --git a/exercises/practice/pascals-triangle/.docs/hints.md b/exercises/practice/pascals-triangle/.docs/hints.md
new file mode 100644
index 00000000..235e786f
--- /dev/null
+++ b/exercises/practice/pascals-triangle/.docs/hints.md
@@ -0,0 +1,10 @@
+# Hints
+
+## General
+
+- A more detailed description of recursive programming can be found [here][g4g]
+- This exercise involves a test to ensure that you used a recursive solution
+- If you are having trouble completing this exercise, try [using a loop first, and then convert it into a recursive solution][educative]
+
+[g4g]: https://www.geeksforgeeks.org/recursion/
+[educative]: https://www.educative.io/collection/page/6151088528949248/4547996664463360/6292303276670976
diff --git a/exercises/practice/pascals-triangle/.docs/instructions.append.md b/exercises/practice/pascals-triangle/.docs/instructions.append.md
new file mode 100644
index 00000000..3b236ae4
--- /dev/null
+++ b/exercises/practice/pascals-triangle/.docs/instructions.append.md
@@ -0,0 +1,43 @@
+# Instructions append
+
+## How this Exercise is Implemented in Python: Recursion
+
+This exercise is designed to be completed using [concept:python/recursion](), rather than loops.
+A recursive function is a function that calls itself, which is useful when solving problems that are defined in terms of themselves.
+To avoid infinite recursion (or more specifically, to avoid overflowing the stack), something called a "base case" is used.
+When the base case is reached, a non-recursive value is returned, which allows the previous function call to resolve and return its value, and so on, rippling back down the stack until the first function call returns the answer.
+We could write a recursive function to find the answer to 5! (i.e. 5 * 4 * 3 * 2 * 1) like so:
+
+````python
+def factorial(number):
+  if number <= 1:  # base case
+    return 1
+
+  return number * factorial(number - 1) # recursive case
+
+print(factorial(5)) # returns 120
+````
+
+Finally, it should be noted that Python limits the number of times recursive calls can be made (1000 by default) and does not optimize for [tail recursion][tail-recursion].
+
+## Exception messages
+
+Sometimes it is necessary to [raise an exception][raising].
+When you do this, you should always include a **meaningful error message** to indicate what the source of the error is.
+This makes your code more readable and helps significantly with debugging.
+For situations where you know that the error source will be a certain type, you can choose to raise one of the [built in error types][built-in-errors], but should still include a meaningful message.
+
+This particular exercise requires that you use the [raise statement][raise-statement] to "throw" multiple `ValueErrors` if the `rows()` function is passed a negative number.
+The tests will only pass if you both `raise` the `exception` and include a message with it.
+
+To raise a `ValueError` with a message, write the message as an argument to the `exception` type:
+
+```python
+# if the rows function is passed a negative number.
+raise ValueError("number of rows is negative")
+```
+
+[built-in-errors]: https://docs.python.org/3/library/exceptions.html#base-classes
+[raise-statement]: https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement
+[raising]: https://docs.python.org/3/tutorial/errors.html#raising-exceptions
+[tail-recursion]: https://www.geeksforgeeks.org/tail-recursion/
diff --git a/exercises/practice/pascals-triangle/.meta/config.json b/exercises/practice/pascals-triangle/.meta/config.json
index 167b937c..4793e16f 100644
--- a/exercises/practice/pascals-triangle/.meta/config.json
+++ b/exercises/practice/pascals-triangle/.meta/config.json
@@ -1,7 +1,8 @@
 {
   "blurb": "Compute Pascal's triangle up to a given number of rows.",
   "authors": [
-    "betegelse"
+    "betegelse",
+    "PaulT89"
   ],
   "contributors": [
     "behrtam",
diff --git a/exercises/practice/pascals-triangle/.meta/example.py b/exercises/practice/pascals-triangle/.meta/example.py
index 70f3aec1..9181199b 100644
--- a/exercises/practice/pascals-triangle/.meta/example.py
+++ b/exercises/practice/pascals-triangle/.meta/example.py
@@ -1,12 +1,8 @@
-def rows(row_count):
+def rows(row_count, previous_row=[1]):
     if row_count < 0:
-        return None
+        raise ValueError("number of rows is negative")
     elif row_count == 0:
         return []
-    row_list = []
-    for idx in range(row_count):
-        ronald = [1]
-        for edx in range(idx):
-            ronald.append(sum(row_list[-1][edx:edx+2]))
-        row_list.append(ronald)
-    return row_list
+    temp_row = previous_row + [0]
+    new_row = list(map(sum, zip(temp_row, temp_row[::-1])))
+    return [previous_row] + rows(row_count - 1, new_row)
diff --git a/exercises/practice/pascals-triangle/.meta/template.j2 b/exercises/practice/pascals-triangle/.meta/template.j2
new file mode 100644
index 00000000..0041fd53
--- /dev/null
+++ b/exercises/practice/pascals-triangle/.meta/template.j2
@@ -0,0 +1,39 @@
+{%- import "generator_macros.j2" as macros with context -%}
+import sys
+{{ macros.header() }}
+
+TRIANGLE = [
+[1],
+[1, 1],
+[1, 2, 1],
+[1, 3, 3, 1],
+[1, 4, 6, 4, 1],
+[1, 5, 10, 10, 5, 1],
+[1, 6, 15, 20, 15, 6, 1],
+[1, 7, 21, 35, 35, 21, 7, 1],
+[1, 8, 28, 56, 70, 56, 28, 8, 1],
+[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
+]
+
+class {{ exercise | camel_case }}Test(unittest.TestCase):
+{%- for case in cases %}
+    def test_{{ case["description"] | to_snake }}(self):
+        self.assertEqual({{ case["property"] }}({{ case["input"]["count"] }}), TRIANGLE[:{{ case["input"]["count"] }}])
+{% endfor %}
+
+# Additional tests for this track
+{%- for case in additional_cases %}
+    def test_{{ case["description"] | to_snake }}(self):
+    {%- if case is error_case %}
+        with self.assertRaises({{ case["expected"]["type"] }}) as err:
+        {%- if case["input"]["count"] == "OVERFLOW" %}
+            rows(sys.getrecursionlimit() + 10)
+            self.assertEqual(type(err.exception), RecursionError)
+            self.assertEqual(err.exception.args[0][:32], "maximum recursion depth exceeded")
+        {%- else %}
+            rows(-1)
+            self.assertEqual(type(err.exception), ValueError)
+            self.assertEqual(err.exception.args[0], "number of rows is negative")
+        {%- endif %}
+    {%- endif %}
+{% endfor %}
diff --git a/exercises/practice/react/.meta/config.json b/exercises/practice/react/.meta/config.json
index 71e3b58a..6ecc1424 100644
--- a/exercises/practice/react/.meta/config.json
+++ b/exercises/practice/react/.meta/config.json
@@ -6,7 +6,9 @@
     "Dog",
     "N-Parsons",
     "Nishant23",
-    "tqa236"
+    "tqa236",
+    "meatball133",
+    "Bethanyg"
   ],
   "files": {
     "solution": [
diff --git a/exercises/practice/react/.meta/template.j2 b/exercises/practice/react/.meta/template.j2
new file mode 100644
index 00000000..f9fb637c
--- /dev/null
+++ b/exercises/practice/react/.meta/template.j2
@@ -0,0 +1,62 @@
+{%- import "generator_macros.j2" as macros with context -%}
+from functools import partial
+{% macro test_case(case) -%}
+    {%- set input = case["input"] -%}
+    {%- set callback = [] -%}
+    {%- for operation in input["operations"] -%}
+        {%- if operation["type"] == "add_callback" -%}
+            {% set callback = callback.append(operation["name"]) %}
+        {%- endif -%}
+    {%- endfor -%}
+    def test_{{ case["description"] | to_snake }}(self):
+        {%- for cell in input["cells"] -%}
+            {%- if cell["type"] == "input" %}
+        {{ cell["name"] }} = InputCell({{ cell["initial_value"] }})
+            {%- elif cell["type"] == "compute" -%}
+                {%- if "if" in cell["compute_function"] %}
+        output = ComputeCell([input], lambda inputs: 111 if inputs[0] < 3 else 222)
+                {%- else %}
+        {{ cell["name"] }} = ComputeCell([{%- for input in cell["inputs"] -%}{{ input }},{%- endfor -%}], lambda inputs: {{ cell["compute_function"] }})
+                {%- endif -%}
+            {%- endif -%}
+        {%- endfor -%}
+        {%- if callback -%}
+        {%- for _ in callback %}
+        cb{{ loop.index0 +1 }}_observer = []
+        {%- endfor -%}
+        {%- endif %}
+        {% for sub_callback in callback -%}
+        {{ sub_callback }} = self.callback_factory(cb{{ loop.index0 + 1 }}_observer)
+        {% endfor -%}
+        {%- for operation in input["operations"] -%}
+            {%- if operation["type"] == "add_callback" or  operation["type"] == "remove_callback" -%}
+                {{ operation["cell"] }}.{{ operation["type"] }}({{ operation["name"] }})
+            {%- elif operation["type"] == "expect_cell_value" -%}
+                self.assertEqual({{ operation["cell"] }}.value, {{ operation["value"] }})
+            {%- elif operation["type"] == "set_value" -%}
+                {{ operation["cell"] }}.value = {{ operation["value"] }}
+                {%- if operation["expect_callbacks_not_to_be_called"] %}
+                    {%- if callback | length == 3 %}
+        self.assertEqual(len(cb{{ operation["expect_callbacks_not_to_be_called"][0][-1] }}_observer), 1)
+                    {%- else %}
+        self.assertEqual(cb{{ operation["expect_callbacks_not_to_be_called"][0][-1] }}_observer, [])
+                    {%- endif %}
+                {%- endif -%}
+                {%- for exp_callback in operation["expect_callbacks"] %}
+        self.assertEqual(cb{{ exp_callback[-1] }}_observer[-1], {{ operation["expect_callbacks"][exp_callback] }})
+                {%- endfor -%}
+            {%- endif %}
+        {% endfor -%}
+{%- endmacro %}
+{{ macros.header(["InputCell", "ComputeCell"])}}
+
+class {{ exercise | camel_case }}Test(unittest.TestCase):
+    {% for case in cases -%}
+        {{ test_case(case) }}
+    {% endfor %}
+
+     # Utility functions.
+    def callback_factory(self, observer):
+        def callback(observer, value):
+            observer.append(value)
+        return partial(callback, observer)
diff --git a/exercises/practice/react/react.py b/exercises/practice/react/react.py
index 03ff02e7..ab6be311 100644
--- a/exercises/practice/react/react.py
+++ b/exercises/practice/react/react.py
@@ -12,3 +12,4 @@ class ComputeCell:
 
     def remove_callback(self, callback):
         pass
+    
\ No newline at end of file
diff --git a/exercises/practice/square-root/.docs/instructions.append.md b/exercises/practice/square-root/.docs/instructions.append.md
new file mode 100644
index 00000000..535038e4
--- /dev/null
+++ b/exercises/practice/square-root/.docs/instructions.append.md
@@ -0,0 +1,16 @@
+# Instructions append
+
+## How this Exercise is Structured in Python
+
+
+Python offers a wealth of mathematical functions in the form of the [math module][math-module] and built-ins such as [`pow()`][pow] and [`sum()`][sum].
+However, we'd like you to consider the challenge of solving this exercise without those built-ins or modules.
+
+While there is a mathematical formula that will find the square root of _any_ number, we have gone the route of only testing [natural numbers][nautral-number] (positive integers).
+
+
+[math-module]: https://docs.python.org/3/library/math.html
+[pow]: https://docs.python.org/3/library/functions.html#pow
+[sum]: https://docs.python.org/3/library/functions.html#sum
+[nautral-number]: https://en.wikipedia.org/wiki/Natural_number
+
diff --git a/exercises/practice/square-root/.docs/instructions.md b/exercises/practice/square-root/.docs/instructions.md
new file mode 100644
index 00000000..e9905e9d
--- /dev/null
+++ b/exercises/practice/square-root/.docs/instructions.md
@@ -0,0 +1,13 @@
+# Instructions
+
+Given a natural radicand, return its square root.
+
+Note that the term "radicand" refers to the number for which the root is to be determined.
+That is, it is the number under the root symbol.
+
+Check out the Wikipedia pages on [square root][square-root] and [methods of computing square roots][computing-square-roots].
+
+Recall also that natural numbers are positive real whole numbers (i.e. 1, 2, 3 and up).
+
+[square-root]: https://en.wikipedia.org/wiki/Square_root
+[computing-square-roots]: https://en.wikipedia.org/wiki/Methods_of_computing_square_roots
diff --git a/exercises/practice/square-root/.meta/config.json b/exercises/practice/square-root/.meta/config.json
new file mode 100644
index 00000000..b1b9630b
--- /dev/null
+++ b/exercises/practice/square-root/.meta/config.json
@@ -0,0 +1,21 @@
+{
+  "authors": [
+    "meatball133",
+    "Bethanyg"
+  ],
+  "contributors": [],
+  "files": {
+    "solution": [
+      "square_root.py"
+    ],
+    "test": [
+      "square_root_test.py"
+    ],
+    "example": [
+      ".meta/example.py"
+    ]
+  },
+  "blurb": "Given a natural radicand, return its square root.",
+  "source": "wolf99",
+  "source_url": "https://github.com/exercism/problem-specifications/pull/1582"
+}
diff --git a/exercises/practice/square-root/.meta/example.py b/exercises/practice/square-root/.meta/example.py
new file mode 100644
index 00000000..94cd0fdc
--- /dev/null
+++ b/exercises/practice/square-root/.meta/example.py
@@ -0,0 +1,5 @@
+def square_root(number):
+    n = 0
+    while n ** 2 != number:
+        n += 1
+    return n
diff --git a/exercises/practice/square-root/.meta/template.j2 b/exercises/practice/square-root/.meta/template.j2
new file mode 100644
index 00000000..93343178
--- /dev/null
+++ b/exercises/practice/square-root/.meta/template.j2
@@ -0,0 +1,15 @@
+{%- import "generator_macros.j2" as macros with context -%}
+{% macro test_case(case) -%}
+    {%- set input = case["input"] -%}
+    def test_{{ case["description"] | to_snake }}(self):
+        self.assertEqual(
+            {{ case["property"] | to_snake }}({{ case["input"]["radicand"] }}),
+            {{ case["expected"] }}
+        )
+{%- endmacro %}
+{{ macros.header()}}
+
+class {{ exercise | camel_case }}Test(unittest.TestCase):
+    {% for case in cases -%}
+        {{ test_case(case) }}
+    {% endfor %}
diff --git a/exercises/practice/square-root/square_root.py b/exercises/practice/square-root/square_root.py
new file mode 100644
index 00000000..0a2fc389
--- /dev/null
+++ b/exercises/practice/square-root/square_root.py
@@ -0,0 +1,2 @@
+def square_root(number):
+    pass


Test patch:
diff --git a/exercises/practice/pascals-triangle/.meta/additional_tests.json b/exercises/practice/pascals-triangle/.meta/additional_tests.json
new file mode 100644
index 00000000..7c1375ab
--- /dev/null
+++ b/exercises/practice/pascals-triangle/.meta/additional_tests.json
@@ -0,0 +1,26 @@
+{
+  "cases": [
+    {
+      "description": "negative rows are invalid",
+      "property": "rows",
+      "input": {
+        "count": -1
+      },
+      "expected": {
+        "error": "number of rows is negative",
+        "type": "ValueError"
+      }
+    },
+    {
+      "description": "solution is recursive",
+      "property": "rows",
+      "input": {
+        "count": "OVERFLOW"
+      },
+      "expected": {
+        "error": "maximum recursion depth exceeded",
+        "type": "RecursionError"
+      }
+    }
+  ]
+}
diff --git a/exercises/practice/pascals-triangle/pascals_triangle_test.py b/exercises/practice/pascals-triangle/pascals_triangle_test.py
index c2c50681..dc7b7ce1 100644
--- a/exercises/practice/pascals-triangle/pascals_triangle_test.py
+++ b/exercises/practice/pascals-triangle/pascals_triangle_test.py
@@ -1,9 +1,11 @@
+import sys
 import unittest
 
-from pascals_triangle import rows
+from pascals_triangle import (
+    rows,
+)
 
-
-# Tests adapted from `problem-specifications//canonical-data.json` @ v1.2.0
+# Tests adapted from `problem-specifications//canonical-data.json`
 
 TRIANGLE = [
     [1],
@@ -15,13 +17,13 @@ TRIANGLE = [
     [1, 6, 15, 20, 15, 6, 1],
     [1, 7, 21, 35, 35, 21, 7, 1],
     [1, 8, 28, 56, 70, 56, 28, 8, 1],
-    [1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
+    [1, 9, 36, 84, 126, 126, 84, 36, 9, 1],
 ]
 
 
 class PascalsTriangleTest(unittest.TestCase):
     def test_zero_rows(self):
-        self.assertEqual(rows(0), [])
+        self.assertEqual(rows(0), TRIANGLE[:0])
 
     def test_single_row(self):
         self.assertEqual(rows(1), TRIANGLE[:1])
@@ -44,9 +46,17 @@ class PascalsTriangleTest(unittest.TestCase):
     def test_ten_rows(self):
         self.assertEqual(rows(10), TRIANGLE[:10])
 
-    def test_negative_rows(self):
-        self.assertEqual(rows(-1), None)
-
-
-if __name__ == '__main__':
-    unittest.main()
+    # Additional tests for this track
+    def test_negative_rows_are_invalid(self):
+        with self.assertRaises(ValueError) as err:
+            rows(-1)
+            self.assertEqual(type(err.exception), ValueError)
+            self.assertEqual(err.exception.args[0], "number of rows is negative")
+
+    def test_solution_is_recursive(self):
+        with self.assertRaises(RecursionError) as err:
+            rows(sys.getrecursionlimit() + 10)
+            self.assertEqual(type(err.exception), RecursionError)
+            self.assertEqual(
+                err.exception.args[0][:32], "maximum recursion depth exceeded"
+            )
diff --git a/exercises/practice/react/react_test.py b/exercises/practice/react/react_test.py
index 7c1870cf..2ba86a80 100644
--- a/exercises/practice/react/react_test.py
+++ b/exercises/practice/react/react_test.py
@@ -1,202 +1,270 @@
-import unittest
 from functools import partial
 
-from react import InputCell, ComputeCell
+import unittest
+
+from react import (
+    InputCell,
+    ComputeCell,
+)
 
+# Tests adapted from `problem-specifications//canonical-data.json`
 
-# Tests adapted from `problem-specifications//canonical-data.json` @ v2.0.0
 
 class ReactTest(unittest.TestCase):
-
     def test_input_cells_have_a_value(self):
-        input_ = InputCell(10)
-        self.assertEqual(input_.value, 10)
+        input = InputCell(10)
+        self.assertEqual(input.value, 10)
 
-    def test_can_set_input_cell_value(self):
-        input_ = InputCell(4)
-        input_.value = 20
-        self.assertEqual(input_.value, 20)
+    def test_an_input_cell_s_value_can_be_set(self):
+        input = InputCell(4)
+        input.value = 20
+        self.assertEqual(input.value, 20)
 
     def test_compute_cells_calculate_initial_value(self):
-        input_ = InputCell(1)
-        output = ComputeCell([input_], lambda inputs: inputs[0] + 1)
+        input = InputCell(1)
+        output = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] + 1,
+        )
         self.assertEqual(output.value, 2)
 
-    def test_compute_cells_take_inputs_in_right_order(self):
+    def test_compute_cells_take_inputs_in_the_right_order(self):
         one = InputCell(1)
         two = InputCell(2)
         output = ComputeCell(
-            [one, two],
-            lambda inputs: inputs[0] + inputs[1]*10
+            [
+                one,
+                two,
+            ],
+            lambda inputs: inputs[0] + inputs[1] * 10,
         )
         self.assertEqual(output.value, 21)
 
     def test_compute_cells_update_value_when_dependencies_are_changed(self):
-        input_ = InputCell(1)
-        output = ComputeCell([input_], lambda inputs: inputs[0] + 1)
-
-        input_.value = 3
+        input = InputCell(1)
+        output = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] + 1,
+        )
+        input.value = 3
         self.assertEqual(output.value, 4)
 
     def test_compute_cells_can_depend_on_other_compute_cells(self):
-        input_ = InputCell(1)
-        times_two = ComputeCell([input_], lambda inputs: inputs[0] * 2)
-        times_thirty = ComputeCell([input_], lambda inputs: inputs[0] * 30)
+        input = InputCell(1)
+        times_two = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] * 2,
+        )
+        times_thirty = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] * 30,
+        )
         output = ComputeCell(
-            [times_two, times_thirty],
-            lambda inputs: inputs[0] + inputs[1]
+            [
+                times_two,
+                times_thirty,
+            ],
+            lambda inputs: inputs[0] + inputs[1],
         )
-
         self.assertEqual(output.value, 32)
-        input_.value = 3
+        input.value = 3
         self.assertEqual(output.value, 96)
 
     def test_compute_cells_fire_callbacks(self):
-        input_ = InputCell(1)
-        output = ComputeCell([input_], lambda inputs: inputs[0] + 1)
-
-        observer = []
-        callback1 = self.callback_factory(observer)
-
-        output.add_callback(callback1)
-        input_.value = 3
-        self.assertEqual(observer[-1], 4)
-
-    def test_callbacks_only_fire_on_change(self):
-        input_ = InputCell(1)
+        input = InputCell(1)
         output = ComputeCell(
-            [input_],
-            lambda inputs: 111 if inputs[0] < 3 else 222
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] + 1,
         )
+        cb1_observer = []
+        callback1 = self.callback_factory(cb1_observer)
+        output.add_callback(callback1)
+        input.value = 3
+        self.assertEqual(cb1_observer[-1], 4)
 
-        observer = []
-        callback1 = self.callback_factory(observer)
-
+    def test_callback_cells_only_fire_on_change(self):
+        input = InputCell(1)
+        output = ComputeCell([input], lambda inputs: 111 if inputs[0] < 3 else 222)
+        cb1_observer = []
+        callback1 = self.callback_factory(cb1_observer)
         output.add_callback(callback1)
-        input_.value = 2
-        self.assertEqual(observer, [])
-        input_.value = 4
-        self.assertEqual(observer[-1], 222)
+        input.value = 2
+        self.assertEqual(cb1_observer, [])
+        input.value = 4
+        self.assertEqual(cb1_observer[-1], 222)
 
     def test_callbacks_do_not_report_already_reported_values(self):
-        input_ = InputCell(1)
-        output = ComputeCell([input_], lambda inputs: inputs[0] + 1)
-
-        observer = []
-        callback1 = self.callback_factory(observer)
-
+        input = InputCell(1)
+        output = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] + 1,
+        )
+        cb1_observer = []
+        callback1 = self.callback_factory(cb1_observer)
         output.add_callback(callback1)
-        input_.value = 2
-        self.assertEqual(observer[-1], 3)
-        input_.value = 3
-        self.assertEqual(observer[-1], 4)
+        input.value = 2
+        self.assertEqual(cb1_observer[-1], 3)
+        input.value = 3
+        self.assertEqual(cb1_observer[-1], 4)
 
     def test_callbacks_can_fire_from_multiple_cells(self):
-        input_ = InputCell(1)
-        plus_one = ComputeCell([input_], lambda inputs: inputs[0] + 1)
-        minus_one = ComputeCell([input_], lambda inputs: inputs[0] - 1)
-
-        cb1_observer, cb2_observer = [], []
+        input = InputCell(1)
+        plus_one = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] + 1,
+        )
+        minus_one = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] - 1,
+        )
+        cb1_observer = []
+        cb2_observer = []
         callback1 = self.callback_factory(cb1_observer)
         callback2 = self.callback_factory(cb2_observer)
-
         plus_one.add_callback(callback1)
         minus_one.add_callback(callback2)
-        input_.value = 10
-
+        input.value = 10
         self.assertEqual(cb1_observer[-1], 11)
         self.assertEqual(cb2_observer[-1], 9)
 
     def test_callbacks_can_be_added_and_removed(self):
-        input_ = InputCell(11)
-        output = ComputeCell([input_], lambda inputs: inputs[0] + 1)
-
-        cb1_observer, cb2_observer, cb3_observer = [], [], []
+        input = InputCell(11)
+        output = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] + 1,
+        )
+        cb1_observer = []
+        cb2_observer = []
+        cb3_observer = []
         callback1 = self.callback_factory(cb1_observer)
         callback2 = self.callback_factory(cb2_observer)
         callback3 = self.callback_factory(cb3_observer)
-
         output.add_callback(callback1)
         output.add_callback(callback2)
-        input_.value = 31
+        input.value = 31
         self.assertEqual(cb1_observer[-1], 32)
         self.assertEqual(cb2_observer[-1], 32)
-
         output.remove_callback(callback1)
         output.add_callback(callback3)
-        input_.value = 41
+        input.value = 41
+        self.assertEqual(len(cb1_observer), 1)
         self.assertEqual(cb2_observer[-1], 42)
         self.assertEqual(cb3_observer[-1], 42)
 
-        # Expect callback1 not to be called.
-        self.assertEqual(len(cb1_observer), 1)
-
-    def test_removing_a_callback_multiple_times(self):
-        """Guard against incorrect implementations which store their
-        callbacks in an array."""
-        input_ = InputCell(1)
-        output = ComputeCell([input_], lambda inputs: inputs[0] + 1)
-
-        cb1_observer, cb2_observer = [], []
+    def test_removing_a_callback_multiple_times_doesn_t_interfere_with_other_callbacks(
+        self,
+    ):
+        input = InputCell(1)
+        output = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] + 1,
+        )
+        cb1_observer = []
+        cb2_observer = []
         callback1 = self.callback_factory(cb1_observer)
         callback2 = self.callback_factory(cb2_observer)
-
         output.add_callback(callback1)
         output.add_callback(callback2)
         output.remove_callback(callback1)
         output.remove_callback(callback1)
         output.remove_callback(callback1)
-        input_.value = 2
-
+        input.value = 2
         self.assertEqual(cb1_observer, [])
         self.assertEqual(cb2_observer[-1], 3)
 
-    def test_callbacks_should_only_be_called_once(self):
-        """Guard against incorrect implementations which call a callback
-        function multiple times when multiple dependencies change."""
-        input_ = InputCell(1)
-        plus_one = ComputeCell([input_], lambda inputs: inputs[0] + 1)
-        minus_one1 = ComputeCell([input_], lambda inputs: inputs[0] - 1)
-        minus_one2 = ComputeCell([minus_one1], lambda inputs: inputs[0] - 1)
+    def test_callbacks_should_only_be_called_once_even_if_multiple_dependencies_change(
+        self,
+    ):
+        input = InputCell(1)
+        plus_one = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] + 1,
+        )
+        minus_one1 = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] - 1,
+        )
+        minus_one2 = ComputeCell(
+            [
+                minus_one1,
+            ],
+            lambda inputs: inputs[0] - 1,
+        )
         output = ComputeCell(
-            [plus_one, minus_one2],
-            lambda inputs: inputs[0] * inputs[1]
+            [
+                plus_one,
+                minus_one2,
+            ],
+            lambda inputs: inputs[0] * inputs[1],
         )
-
-        observer = []
-        callback1 = self.callback_factory(observer)
-
+        cb1_observer = []
+        callback1 = self.callback_factory(cb1_observer)
         output.add_callback(callback1)
-        input_.value = 4
-        self.assertEqual(observer[-1], 10)
-
-    def test_callbacks_not_called_so_long_as_output_not_changed(self):
-        """Guard against incorrect implementations which call callbacks
-        if dependencies change but output value doesn't change."""
-        input_ = InputCell(1)
-        plus_one = ComputeCell([input_], lambda inputs: inputs[0] + 1)
-        minus_one = ComputeCell([input_], lambda inputs: inputs[0] - 1)
+        input.value = 4
+        self.assertEqual(cb1_observer[-1], 10)
+
+    def test_callbacks_should_not_be_called_if_dependencies_change_but_output_value_doesn_t_change(
+        self,
+    ):
+        input = InputCell(1)
+        plus_one = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] + 1,
+        )
+        minus_one = ComputeCell(
+            [
+                input,
+            ],
+            lambda inputs: inputs[0] - 1,
+        )
         always_two = ComputeCell(
-            [plus_one, minus_one],
-            lambda inputs: inputs[0] - inputs[1]
+            [
+                plus_one,
+                minus_one,
+            ],
+            lambda inputs: inputs[0] - inputs[1],
         )
-
-        observer = []
-        callback1 = self.callback_factory(observer)
-
+        cb1_observer = []
+        callback1 = self.callback_factory(cb1_observer)
         always_two.add_callback(callback1)
-        input_.value = 2
-        input_.value = 3
-        input_.value = 4
-        input_.value = 5
-        self.assertEqual(observer, [])
+        input.value = 2
+        self.assertEqual(cb1_observer, [])
+        input.value = 3
+        self.assertEqual(cb1_observer, [])
+        input.value = 4
+        self.assertEqual(cb1_observer, [])
+        input.value = 5
+        self.assertEqual(cb1_observer, [])
 
     # Utility functions.
     def callback_factory(self, observer):
         def callback(observer, value):
             observer.append(value)
-        return partial(callback, observer)
-
 
-if __name__ == '__main__':
-    unittest.main()
+        return partial(callback, observer)
diff --git a/exercises/practice/square-root/.meta/tests.toml b/exercises/practice/square-root/.meta/tests.toml
new file mode 100644
index 00000000..ead7882f
--- /dev/null
+++ b/exercises/practice/square-root/.meta/tests.toml
@@ -0,0 +1,28 @@
+# This is an auto-generated file.
+#
+# Regenerating this file via `configlet sync` will:
+# - Recreate every `description` key/value pair
+# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
+# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
+# - Preserve any other key/value pair
+#
+# As user-added comments (using the # character) will be removed when this file
+# is regenerated, comments can be added via a `comment` key.
+
+[9b748478-7b0a-490c-b87a-609dacf631fd]
+description = "root of 1"
+
+[7d3aa9ba-9ac6-4e93-a18b-2e8b477139bb]
+description = "root of 4"
+
+[6624aabf-3659-4ae0-a1c8-25ae7f33c6ef]
+description = "root of 25"
+
+[93beac69-265e-4429-abb1-94506b431f81]
+description = "root of 81"
+
+[fbddfeda-8c4f-4bc4-87ca-6991af35360e]
+description = "root of 196"
+
+[c03d0532-8368-4734-a8e0-f96a9eb7fc1d]
+description = "root of 65025"
diff --git a/exercises/practice/square-root/square_root_test.py b/exercises/practice/square-root/square_root_test.py
new file mode 100644
index 00000000..635728fe
--- /dev/null
+++ b/exercises/practice/square-root/square_root_test.py
@@ -0,0 +1,27 @@
+import unittest
+
+from square_root import (
+    square_root,
+)
+
+# Tests adapted from `problem-specifications//canonical-data.json`
+
+
+class SquareRootTest(unittest.TestCase):
+    def test_root_of_1(self):
+        self.assertEqual(square_root(1), 1)
+
+    def test_root_of_4(self):
+        self.assertEqual(square_root(4), 2)
+
+    def test_root_of_25(self):
+        self.assertEqual(square_root(25), 5)
+
+    def test_root_of_81(self):
+        self.assertEqual(square_root(81), 9)
+
+    def test_root_of_196(self):
+        self.assertEqual(square_root(196), 14)
+
+    def test_root_of_65025(self):
+        self.assertEqual(square_root(65025), 255)