From 96db0939c1756bcdea186e4d09624b8e861a8795 Mon Sep 17 00:00:00 2001
From: "Bradley M. Small" <bradley_small@hotmail.com>
Date: Thu, 27 Feb 2020 20:49:36 -0500
Subject: [PATCH] Adding python work

---
 python/armstrong-numbers/README.md            |  59 ++++++++
 python/armstrong-numbers/armstrong_numbers.py |   5 +
 .../armstrong_numbers_test.py                 |  38 ++++++
 python/hello-world/README.md                  |  62 +++++++++
 python/hello-world/hello_world.py             |   2 +
 python/hello-world/hello_world_test.py        |  14 ++
 python/high-scores/README.md                  |  58 ++++++++
 python/high-scores/high_scores.py             |  10 ++
 python/high-scores/high_scores_test.py        |  46 +++++++
 python/pangram/README.md                      |  56 ++++++++
 python/pangram/pangram.py                     |   3 +
 python/pangram/pangram_test.py                |  46 +++++++
 python/perfect-numbers/README.md              |  65 +++++++++
 python/perfect-numbers/fact.py                |   6 +
 python/perfect-numbers/fact_test.py           |  72 ++++++++++
 python/perfect-numbers/perfect_numbers.py     |  16 +++
 .../perfect-numbers/perfect_numbers_test.py   |  62 +++++++++
 python/protein-translation/README.md          |  89 +++++++++++++
 .../protein_translation.py                    |  34 +++++
 .../protein_translation_test.py               | 126 ++++++++++++++++++
 python/raindrops/README.md                    |  63 +++++++++
 python/raindrops/raindrops.py                 |   9 ++
 python/raindrops/raindrops_test.py            |  67 ++++++++++
 python/robot-name/README.md                   |  63 +++++++++
 python/robot-name/robot_name.py               |  22 +++
 python/robot-name/robot_name_test.py          |  51 +++++++
 python/two-fer/README.md                      |  72 ++++++++++
 python/two-fer/two_fer.py                     |   2 +
 python/two-fer/two_fer_test.py                |  20 +++
 29 files changed, 1238 insertions(+)
 create mode 100644 python/armstrong-numbers/README.md
 create mode 100644 python/armstrong-numbers/armstrong_numbers.py
 create mode 100644 python/armstrong-numbers/armstrong_numbers_test.py
 create mode 100644 python/hello-world/README.md
 create mode 100644 python/hello-world/hello_world.py
 create mode 100644 python/hello-world/hello_world_test.py
 create mode 100644 python/high-scores/README.md
 create mode 100644 python/high-scores/high_scores.py
 create mode 100644 python/high-scores/high_scores_test.py
 create mode 100644 python/pangram/README.md
 create mode 100644 python/pangram/pangram.py
 create mode 100644 python/pangram/pangram_test.py
 create mode 100644 python/perfect-numbers/README.md
 create mode 100644 python/perfect-numbers/fact.py
 create mode 100644 python/perfect-numbers/fact_test.py
 create mode 100644 python/perfect-numbers/perfect_numbers.py
 create mode 100644 python/perfect-numbers/perfect_numbers_test.py
 create mode 100644 python/protein-translation/README.md
 create mode 100644 python/protein-translation/protein_translation.py
 create mode 100644 python/protein-translation/protein_translation_test.py
 create mode 100644 python/raindrops/README.md
 create mode 100644 python/raindrops/raindrops.py
 create mode 100644 python/raindrops/raindrops_test.py
 create mode 100644 python/robot-name/README.md
 create mode 100644 python/robot-name/robot_name.py
 create mode 100644 python/robot-name/robot_name_test.py
 create mode 100644 python/two-fer/README.md
 create mode 100644 python/two-fer/two_fer.py
 create mode 100644 python/two-fer/two_fer_test.py

diff --git a/python/armstrong-numbers/README.md b/python/armstrong-numbers/README.md
new file mode 100644
index 0000000..a2402fa
--- /dev/null
+++ b/python/armstrong-numbers/README.md
@@ -0,0 +1,59 @@
+# Armstrong Numbers
+
+An [Armstrong number](https://en.wikipedia.org/wiki/Narcissistic_number) is a number that is the sum of its own digits each raised to the power of the number of digits.
+
+For example:
+
+- 9 is an Armstrong number, because `9 = 9^1 = 9`
+- 10 is *not* an Armstrong number, because `10 != 1^2 + 0^2 = 1`
+- 153 is an Armstrong number, because: `153 = 1^3 + 5^3 + 3^3 = 1 + 125 + 27 = 153`
+- 154 is *not* an Armstrong number, because: `154 != 1^3 + 5^3 + 4^3 = 1 + 125 + 64 = 190`
+
+Write some code to determine whether a number is an Armstrong number.
+
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should 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. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest armstrong_numbers_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest armstrong_numbers_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/armstrong-numbers` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+Wikipedia [https://en.wikipedia.org/wiki/Narcissistic_number](https://en.wikipedia.org/wiki/Narcissistic_number)
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/python/armstrong-numbers/armstrong_numbers.py b/python/armstrong-numbers/armstrong_numbers.py
new file mode 100644
index 0000000..8daf120
--- /dev/null
+++ b/python/armstrong-numbers/armstrong_numbers.py
@@ -0,0 +1,5 @@
+def is_armstrong_number(number):
+    digits = list(map(int, str(number)))
+    count = len(digits)
+    sum_raised_digits = sum([d**count for d in digits])
+    return sum_raised_digits == number
diff --git a/python/armstrong-numbers/armstrong_numbers_test.py b/python/armstrong-numbers/armstrong_numbers_test.py
new file mode 100644
index 0000000..e36d220
--- /dev/null
+++ b/python/armstrong-numbers/armstrong_numbers_test.py
@@ -0,0 +1,38 @@
+import unittest
+
+from armstrong_numbers import is_armstrong_number
+
+# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0
+
+
+class ArmstrongNumbersTest(unittest.TestCase):
+    def test_zero_is_an_armstrong_number(self):
+        self.assertIs(is_armstrong_number(0), True)
+
+    def test_single_digit_numbers_are_armstrong_numbers(self):
+        self.assertIs(is_armstrong_number(5), True)
+
+    def test_there_are_no_2_digit_armstrong_numbers(self):
+        self.assertIs(is_armstrong_number(10), False)
+
+    def test_three_digit_number_that_is_an_armstrong_number(self):
+        self.assertIs(is_armstrong_number(153), True)
+
+    def test_three_digit_number_that_is_not_an_armstrong_number(self):
+        self.assertIs(is_armstrong_number(100), False)
+
+    def test_four_digit_number_that_is_an_armstrong_number(self):
+        self.assertIs(is_armstrong_number(9474), True)
+
+    def test_four_digit_number_that_is_not_an_armstrong_number(self):
+        self.assertIs(is_armstrong_number(9475), False)
+
+    def test_seven_digit_number_that_is_an_armstrong_number(self):
+        self.assertIs(is_armstrong_number(9926315), True)
+
+    def test_seven_digit_number_that_is_not_an_armstrong_number(self):
+        self.assertIs(is_armstrong_number(9926314), False)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python/hello-world/README.md b/python/hello-world/README.md
new file mode 100644
index 0000000..d5acfab
--- /dev/null
+++ b/python/hello-world/README.md
@@ -0,0 +1,62 @@
+# Hello World
+
+The classical introductory exercise. Just say "Hello, World!".
+
+["Hello, World!"](http://en.wikipedia.org/wiki/%22Hello,_world!%22_program) is
+the traditional first program for beginning programming in a new language
+or environment.
+
+The objectives are simple:
+
+- Write a function that returns the string "Hello, World!".
+- Run the test suite and make sure that it succeeds.
+- Submit your solution and check it at the website.
+
+If everything goes well, you will be ready to fetch your first real exercise.
+
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should 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. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest hello_world_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest hello_world_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/hello-world` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+This is an exercise to introduce users to using Exercism [http://en.wikipedia.org/wiki/%22Hello,_world!%22_program](http://en.wikipedia.org/wiki/%22Hello,_world!%22_program)
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/python/hello-world/hello_world.py b/python/hello-world/hello_world.py
new file mode 100644
index 0000000..d695ea1
--- /dev/null
+++ b/python/hello-world/hello_world.py
@@ -0,0 +1,2 @@
+def hello():
+    return 'Hello, World!'
diff --git a/python/hello-world/hello_world_test.py b/python/hello-world/hello_world_test.py
new file mode 100644
index 0000000..a6b332d
--- /dev/null
+++ b/python/hello-world/hello_world_test.py
@@ -0,0 +1,14 @@
+import unittest
+
+from hello_world import hello
+
+# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0
+
+
+class HelloWorldTest(unittest.TestCase):
+    def test_say_hi(self):
+        self.assertEqual(hello(), "Hello, World!")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python/high-scores/README.md b/python/high-scores/README.md
new file mode 100644
index 0000000..70e72d8
--- /dev/null
+++ b/python/high-scores/README.md
@@ -0,0 +1,58 @@
+# High Scores
+
+Manage a game player's High Score list.
+
+Your task is to build a high-score component of the classic Frogger game, one of the highest selling and addictive games of all time, and a classic of the arcade era. Your task is to write methods that return the highest score from the list, the last added score and the three highest scores.
+
+In this exercise, you're going to use and manipulate lists. Python lists are very versatile, and you'll find yourself using them again and again in problems both simple and complex.
+
+- [**Data Structures (Python 3 Documentation Tutorial)**](https://docs.python.org/3/tutorial/datastructures.html)
+- [**Lists and Tuples in Python (Real Python)**](https://realpython.com/python-lists-tuples/)
+- [**Python Lists (Google for Education)**](https://developers.google.com/edu/python/lists)
+
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should 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. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest high_scores_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest high_scores_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/high-scores` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+Tribute to the eighties' arcade game Frogger
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/python/high-scores/high_scores.py b/python/high-scores/high_scores.py
new file mode 100644
index 0000000..d5f1f37
--- /dev/null
+++ b/python/high-scores/high_scores.py
@@ -0,0 +1,10 @@
+def latest(scores):
+    return scores[-1]
+
+
+def personal_best(scores):
+    return max(scores)
+
+
+def personal_top_three(scores):
+    return sorted(scores,reverse=True)[:3]
diff --git a/python/high-scores/high_scores_test.py b/python/high-scores/high_scores_test.py
new file mode 100644
index 0000000..9a35c6d
--- /dev/null
+++ b/python/high-scores/high_scores_test.py
@@ -0,0 +1,46 @@
+import unittest
+
+from high_scores import latest, personal_best, personal_top_three
+
+# Tests adapted from `problem-specifications//canonical-data.json` @ v4.0.0
+
+
+class HighScoresTest(unittest.TestCase):
+    def test_latest_score(self):
+        scores = [100, 0, 90, 30]
+        expected = 30
+        self.assertEqual(latest(scores), expected)
+
+    def test_personal_best(self):
+        scores = [40, 100, 70]
+        expected = 100
+        self.assertEqual(personal_best(scores), expected)
+
+    def test_personal_top_three_from_a_list_of_scores(self):
+        scores = [10, 30, 90, 30, 100, 20, 10, 0, 30, 40, 40, 70, 70]
+        expected = [100, 90, 70]
+        self.assertEqual(personal_top_three(scores), expected)
+
+    def test_personal_top_highest_to_lowest(self):
+        scores = [20, 10, 30]
+        expected = [30, 20, 10]
+        self.assertEqual(personal_top_three(scores), expected)
+
+    def test_personal_top_when_there_is_a_tie(self):
+        scores = [40, 20, 40, 30]
+        expected = [40, 40, 30]
+        self.assertEqual(personal_top_three(scores), expected)
+
+    def test_personal_top_when_there_are_less_than_3(self):
+        scores = [30, 70]
+        expected = [70, 30]
+        self.assertEqual(personal_top_three(scores), expected)
+
+    def test_personal_top_when_there_is_only_one(self):
+        scores = [40]
+        expected = [40]
+        self.assertEqual(personal_top_three(scores), expected)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python/pangram/README.md b/python/pangram/README.md
new file mode 100644
index 0000000..b81d824
--- /dev/null
+++ b/python/pangram/README.md
@@ -0,0 +1,56 @@
+# Pangram
+
+Determine if a sentence is a pangram. A pangram (Greek: παν γράμμα, pan gramma,
+"every letter") is a sentence using every letter of the alphabet at least once.
+The best known English pangram is:
+> The quick brown fox jumps over the lazy dog.
+
+The alphabet used consists of ASCII letters `a` to `z`, inclusive, and is case
+insensitive. Input will not contain non-ASCII symbols.
+
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should 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. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest pangram_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest pangram_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/pangram` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+Wikipedia [https://en.wikipedia.org/wiki/Pangram](https://en.wikipedia.org/wiki/Pangram)
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/python/pangram/pangram.py b/python/pangram/pangram.py
new file mode 100644
index 0000000..33b97fd
--- /dev/null
+++ b/python/pangram/pangram.py
@@ -0,0 +1,3 @@
+def is_pangram(sentence):
+    return {letter.lower() for letter in sentence
+            if letter.isalpha()}.__len__() == 26
diff --git a/python/pangram/pangram_test.py b/python/pangram/pangram_test.py
new file mode 100644
index 0000000..4284849
--- /dev/null
+++ b/python/pangram/pangram_test.py
@@ -0,0 +1,46 @@
+import unittest
+
+from pangram import is_pangram
+
+# Tests adapted from `problem-specifications//canonical-data.json` @ v2.0.0
+
+
+class PangramTest(unittest.TestCase):
+    def test_empty_sentence(self):
+        self.assertIs(is_pangram(""), False)
+
+    def test_perfect_lower_case(self):
+        self.assertIs(is_pangram("abcdefghijklmnopqrstuvwxyz"), True)
+
+    def test_only_lower_case(self):
+        self.assertIs(is_pangram("the quick brown fox jumps over the lazy dog"), True)
+
+    def test_missing_the_letter_x(self):
+        self.assertIs(
+            is_pangram("a quick movement of the enemy will jeopardize five gunboats"),
+            False,
+        )
+
+    def test_missing_the_letter_h(self):
+        self.assertIs(is_pangram("five boxing wizards jump quickly at it"), False)
+
+    def test_with_underscores(self):
+        self.assertIs(is_pangram("the_quick_brown_fox_jumps_over_the_lazy_dog"), True)
+
+    def test_with_numbers(self):
+        self.assertIs(
+            is_pangram("the 1 quick brown fox jumps over the 2 lazy dogs"), True
+        )
+
+    def test_missing_letters_replaced_by_numbers(self):
+        self.assertIs(is_pangram("7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"), False)
+
+    def test_mixed_case_and_punctuation(self):
+        self.assertIs(is_pangram('"Five quacking Zephyrs jolt my wax bed."'), True)
+
+    def test_case_insensitive(self):
+        self.assertIs(is_pangram("the quick brown fox jumps over with lazy FX"), False)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python/perfect-numbers/README.md b/python/perfect-numbers/README.md
new file mode 100644
index 0000000..7759f9e
--- /dev/null
+++ b/python/perfect-numbers/README.md
@@ -0,0 +1,65 @@
+# Perfect Numbers
+
+Determine if a number is perfect, abundant, or deficient based on
+Nicomachus' (60 - 120 CE) classification scheme for natural numbers.
+
+The Greek mathematician [Nicomachus](https://en.wikipedia.org/wiki/Nicomachus) devised a classification scheme for natural numbers, identifying each as belonging uniquely to the categories of **perfect**, **abundant**, or **deficient** based on their [aliquot sum](https://en.wikipedia.org/wiki/Aliquot_sum). The aliquot sum is defined as the sum of the factors of a number not including the number itself. For example, the aliquot sum of 15 is (1 + 3 + 5) = 9
+
+- **Perfect**: aliquot sum = number
+  - 6 is a perfect number because (1 + 2 + 3) = 6
+  - 28 is a perfect number because (1 + 2 + 4 + 7 + 14) = 28
+- **Abundant**: aliquot sum > number
+  - 12 is an abundant number because (1 + 2 + 3 + 4 + 6) = 16
+  - 24 is an abundant number because (1 + 2 + 3 + 4 + 6 + 8 + 12) = 36
+- **Deficient**: aliquot sum < number
+  - 8 is a deficient number because (1 + 2 + 4) = 7
+  - Prime numbers are deficient
+
+Implement a way to determine whether a given number is **perfect**. Depending on your language track, you may also need to implement a way to determine whether a given number is **abundant** or **deficient**.
+
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should 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. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest perfect_numbers_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest perfect_numbers_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/perfect-numbers` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+Taken from Chapter 2 of Functional Thinking by Neal Ford. [http://shop.oreilly.com/product/0636920029687.do](http://shop.oreilly.com/product/0636920029687.do)
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/python/perfect-numbers/fact.py b/python/perfect-numbers/fact.py
new file mode 100644
index 0000000..7307e24
--- /dev/null
+++ b/python/perfect-numbers/fact.py
@@ -0,0 +1,6 @@
+"""Fact module."""
+
+
+def fact(num):
+    return [] if not num else sorted(
+            list((x for x in range(1, num+1) if num % x == 0)))
diff --git a/python/perfect-numbers/fact_test.py b/python/perfect-numbers/fact_test.py
new file mode 100644
index 0000000..bdd5ab4
--- /dev/null
+++ b/python/perfect-numbers/fact_test.py
@@ -0,0 +1,72 @@
+"""Unit tests for fact module."""
+
+
+import fact
+
+
+def test_fact_0():
+    assert fact.fact(0) == []
+
+
+def test_fact_1():
+    assert fact.fact(1) == [1]
+
+
+def test_fact_2():
+    assert fact.fact(2) == [1, 2]
+
+
+def test_fact_3():
+    assert fact.fact(3) == [1, 3]
+
+
+def test_fact_4():
+    assert fact.fact(4) == [1, 2, 4]
+
+
+def test_fact_5():
+    assert fact.fact(5) == [1, 5]
+
+
+def test_fact_6():
+    assert fact.fact(6) == [1, 2, 3, 6]
+
+
+def test_fact_12():
+    assert fact.fact(12) == [1, 2, 3, 4, 6, 12]
+
+
+def test_fact_36():
+    assert fact.fact(36) == [1, 2, 3, 4, 6, 9, 12, 18, 36]
+
+
+def test_fact_big():
+    assert fact.fact(33550336) == [
+        1,
+        2,
+        4,
+        8,
+        16,
+        32,
+        64,
+        128,
+        256,
+        512,
+        1024,
+        2048,
+        4096,
+        8191,
+        16382,
+        32764,
+        65528,
+        131056,
+        262112,
+        524224,
+        1048448,
+        2096896,
+        4193792,
+        8387584,
+        16775168,
+        33550336
+    ]
+
diff --git a/python/perfect-numbers/perfect_numbers.py b/python/perfect-numbers/perfect_numbers.py
new file mode 100644
index 0000000..1efa1c8
--- /dev/null
+++ b/python/perfect-numbers/perfect_numbers.py
@@ -0,0 +1,16 @@
+import fact
+
+
+def classify(number):
+    if number < 1:
+        raise ValueError('Must be > 0')
+    if not isinstance(number, int):
+        raise ValueError('Must be natural number')
+
+    aliquot = sum(fact.fact(number)[:-1])
+    if aliquot == number:
+        return 'perfect'
+    if aliquot > number:
+        return 'abundant'
+    if aliquot < number:
+        return 'deficient'
diff --git a/python/perfect-numbers/perfect_numbers_test.py b/python/perfect-numbers/perfect_numbers_test.py
new file mode 100644
index 0000000..2ccb268
--- /dev/null
+++ b/python/perfect-numbers/perfect_numbers_test.py
@@ -0,0 +1,62 @@
+import unittest
+
+from perfect_numbers import classify
+
+# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0
+
+
+class PerfectNumbersTest(unittest.TestCase):
+    def test_smallest_perfect_number_is_classified_correctly(self):
+        self.assertIs(classify(6), "perfect")
+
+    def test_medium_perfect_number_is_classified_correctly(self):
+        self.assertIs(classify(28), "perfect")
+
+    def test_large_perfect_number_is_classified_correctly(self):
+        self.assertIs(classify(33550336), "perfect")
+
+
+class AbundantNumbersTest(unittest.TestCase):
+    def test_smallest_abundant_number_is_classified_correctly(self):
+        self.assertIs(classify(12), "abundant")
+
+    def test_medium_abundant_number_is_classified_correctly(self):
+        self.assertIs(classify(30), "abundant")
+
+    def test_large_abundant_number_is_classified_correctly(self):
+        self.assertIs(classify(33550335), "abundant")
+
+
+class DeficientNumbersTest(unittest.TestCase):
+    def test_smallest_prime_deficient_number_is_classified_correctly(self):
+        self.assertIs(classify(2), "deficient")
+
+    def test_smallest_non_prime_deficient_number_is_classified_correctly(self):
+        self.assertIs(classify(4), "deficient")
+
+    def test_medium_deficient_number_is_classified_correctly(self):
+        self.assertIs(classify(32), "deficient")
+
+    def test_large_deficient_number_is_classified_correctly(self):
+        self.assertIs(classify(33550337), "deficient")
+
+    def test_edge_case_no_factors_other_than_itself_is_classified_correctly(self):
+        self.assertIs(classify(1), "deficient")
+
+
+class InvalidInputsTest(unittest.TestCase):
+    def test_zero_is_rejected_not_a_natural_number(self):
+        with self.assertRaisesWithMessage(ValueError):
+            classify(0)
+
+    def test_negative_integer_is_rejected_not_a_natural_number(self):
+        with self.assertRaisesWithMessage(ValueError):
+            classify(-1)
+
+    # Utility functions
+    def assertRaisesWithMessage(self, exception):
+        return self.assertRaisesRegex(exception, r".+")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python/protein-translation/README.md b/python/protein-translation/README.md
new file mode 100644
index 0000000..24da4be
--- /dev/null
+++ b/python/protein-translation/README.md
@@ -0,0 +1,89 @@
+# Protein Translation
+
+Translate RNA sequences into proteins.
+
+RNA can be broken into three nucleotide sequences called codons, and then translated to a polypeptide like so:
+
+RNA: `"AUGUUUUCU"` => translates to
+
+Codons: `"AUG", "UUU", "UCU"`
+=> which become a polypeptide with the following sequence =>
+
+Protein: `"Methionine", "Phenylalanine", "Serine"`
+
+There are 64 codons which in turn correspond to 20 amino acids; however, all of the codon sequences and resulting amino acids are not important in this exercise.  If it works for one codon, the program should work for all of them.
+However, feel free to expand the list in the test suite to include them all.
+
+There are also three terminating codons (also known as 'STOP' codons); if any of these codons are encountered (by the ribosome), all translation ends and the protein is terminated.
+
+All subsequent codons after are ignored, like this:
+
+RNA: `"AUGUUUUCUUAAAUG"` =>
+
+Codons: `"AUG", "UUU", "UCU", "UAA", "AUG"` =>
+
+Protein: `"Methionine", "Phenylalanine", "Serine"`
+
+Note the stop codon `"UAA"` terminates the translation and the final methionine is not translated into the protein sequence.
+
+Below are the codons and resulting Amino Acids needed for the exercise.
+
+Codon                 | Protein
+:---                  | :---
+AUG                   | Methionine
+UUU, UUC              | Phenylalanine
+UUA, UUG              | Leucine
+UCU, UCC, UCA, UCG    | Serine
+UAU, UAC              | Tyrosine
+UGU, UGC              | Cysteine
+UGG                   | Tryptophan
+UAA, UAG, UGA         | STOP
+
+Learn more about [protein translation on Wikipedia](http://en.wikipedia.org/wiki/Translation_(biology))
+
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should 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. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest protein_translation_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest protein_translation_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/protein-translation` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+Tyler Long
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/python/protein-translation/protein_translation.py b/python/protein-translation/protein_translation.py
new file mode 100644
index 0000000..96e9cc3
--- /dev/null
+++ b/python/protein-translation/protein_translation.py
@@ -0,0 +1,34 @@
+protein_dict = {
+    "AUG": "Methionine",
+    "UUU": "Phenylalanine",
+    "UUC": "Phenylalanine",
+    "UUA": "Leucine",
+    "UUG": "Leucine",
+    "UCU": "Serine",
+    "UCC": "Serine",
+    "UCA": "Serine",
+    "UCG": "Serine",
+    "UAU": "Tyrosine",
+    "UAC": "Tyrosine",
+    "UGU": "Cysteine",
+    "UGC": "Cysteine",
+    "UGG": "Tryptophan",
+    "UAA": "Stop",
+    "UAG": "Stop",
+    "UGA": "Stop",
+}
+
+
+def proteins(strand):
+    protein_list = []
+    codon_list = []
+    for triplet in range(0, len(strand), 3):
+        codon_list.append(strand[triplet:triplet+3])
+
+    for codon in codon_list:
+        protein = protein_dict[codon]
+        if protein == 'Stop':
+            break
+        protein_list.append(protein)
+
+    return protein_list
diff --git a/python/protein-translation/protein_translation_test.py b/python/protein-translation/protein_translation_test.py
new file mode 100644
index 0000000..c14e93d
--- /dev/null
+++ b/python/protein-translation/protein_translation_test.py
@@ -0,0 +1,126 @@
+import unittest
+
+from protein_translation import proteins
+
+# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.1
+
+
+class ProteinTranslationTest(unittest.TestCase):
+    def test_methionine_rna_sequence(self):
+        value = "AUG"
+        expected = ["Methionine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_phenylalanine_rna_sequence_1(self):
+        value = "UUU"
+        expected = ["Phenylalanine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_phenylalanine_rna_sequence_2(self):
+        value = "UUC"
+        expected = ["Phenylalanine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_leucine_rna_sequence_1(self):
+        value = "UUA"
+        expected = ["Leucine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_leucine_rna_sequence_2(self):
+        value = "UUG"
+        expected = ["Leucine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_serine_rna_sequence_1(self):
+        value = "UCU"
+        expected = ["Serine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_serine_rna_sequence_2(self):
+        value = "UCC"
+        expected = ["Serine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_serine_rna_sequence_3(self):
+        value = "UCA"
+        expected = ["Serine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_serine_rna_sequence_4(self):
+        value = "UCG"
+        expected = ["Serine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_tyrosine_rna_sequence_1(self):
+        value = "UAU"
+        expected = ["Tyrosine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_tyrosine_rna_sequence_2(self):
+        value = "UAC"
+        expected = ["Tyrosine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_cysteine_rna_sequence_1(self):
+        value = "UGU"
+        expected = ["Cysteine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_cysteine_rna_sequence_2(self):
+        value = "UGC"
+        expected = ["Cysteine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_tryptophan_rna_sequence(self):
+        value = "UGG"
+        expected = ["Tryptophan"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_stop_codon_rna_sequence_1(self):
+        value = "UAA"
+        expected = []
+        self.assertEqual(proteins(value), expected)
+
+    def test_stop_codon_rna_sequence_2(self):
+        value = "UAG"
+        expected = []
+        self.assertEqual(proteins(value), expected)
+
+    def test_stop_codon_rna_sequence_3(self):
+        value = "UGA"
+        expected = []
+        self.assertEqual(proteins(value), expected)
+
+    def test_translate_rna_strand_into_correct_protein_list(self):
+        value = "AUGUUUUGG"
+        expected = ["Methionine", "Phenylalanine", "Tryptophan"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_translation_stops_if_stop_codon_at_beginning_of_sequence(self):
+        value = "UAGUGG"
+        expected = []
+        self.assertEqual(proteins(value), expected)
+
+    def test_translation_stops_if_stop_codon_at_end_of_two_codon_sequence(self):
+        value = "UGGUAG"
+        expected = ["Tryptophan"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_translation_stops_if_stop_codon_at_end_of_three_codon_sequence(self):
+        value = "AUGUUUUAA"
+        expected = ["Methionine", "Phenylalanine"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_translation_stops_if_stop_codon_in_middle_of_three_codon_sequence(self):
+        value = "UGGUAGUGG"
+        expected = ["Tryptophan"]
+        self.assertEqual(proteins(value), expected)
+
+    def test_translation_stops_if_stop_codon_in_middle_of_six_codon_sequence(self):
+        value = "UGGUGUUAUUAAUGGUUU"
+        expected = ["Tryptophan", "Cysteine", "Tyrosine"]
+        self.assertEqual(proteins(value), expected)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python/raindrops/README.md b/python/raindrops/README.md
new file mode 100644
index 0000000..acfac81
--- /dev/null
+++ b/python/raindrops/README.md
@@ -0,0 +1,63 @@
+# Raindrops
+
+Your task is to convert a number into a string that contains raindrop sounds corresponding to certain potential factors. A factor is a number that evenly divides into another number, leaving no remainder. The simplest way to test if a one number is a factor of another is to use the [modulo operation](https://en.wikipedia.org/wiki/Modulo_operation).
+
+The rules of `raindrops` are that if a given number:
+
+- has 3 as a factor, add 'Pling' to the result.
+- has 5 as a factor, add 'Plang' to the result.
+- has 7 as a factor, add 'Plong' to the result.
+- _does not_ have any of 3, 5, or 7 as a factor, the result should be the digits of the number.
+
+## Examples
+
+- 28 has 7 as a factor, but not 3 or 5, so the result would be "Plong".
+- 30 has both 3 and 5 as factors, but not 7, so the result would be "PlingPlang".
+- 34 is not factored by 3, 5, or 7, so the result would be "34".
+
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should 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. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest raindrops_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest raindrops_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/raindrops` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+A variation on FizzBuzz, a famous technical interview question that is intended to weed out potential candidates. That question is itself derived from Fizz Buzz, a popular children's game for teaching division. [https://en.wikipedia.org/wiki/Fizz_buzz](https://en.wikipedia.org/wiki/Fizz_buzz)
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/python/raindrops/raindrops.py b/python/raindrops/raindrops.py
new file mode 100644
index 0000000..50a7f87
--- /dev/null
+++ b/python/raindrops/raindrops.py
@@ -0,0 +1,9 @@
+def convert(number):
+    conc_dict = {3: "Pling", 5: "Plang", 7: "Plong"}
+
+    hold = ""
+    for fact, name in conc_dict.items():
+        if number % fact == 0:
+            hold += name
+
+    return hold or str(number)
diff --git a/python/raindrops/raindrops_test.py b/python/raindrops/raindrops_test.py
new file mode 100644
index 0000000..169015f
--- /dev/null
+++ b/python/raindrops/raindrops_test.py
@@ -0,0 +1,67 @@
+import unittest
+
+from raindrops import convert
+
+# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0
+
+
+class RaindropsTest(unittest.TestCase):
+    def test_the_sound_for_1_is_1(self):
+        self.assertEqual(convert(1), "1")
+
+    def test_the_sound_for_3_is_pling(self):
+        self.assertEqual(convert(3), "Pling")
+
+    def test_the_sound_for_5_is_plang(self):
+        self.assertEqual(convert(5), "Plang")
+
+    def test_the_sound_for_7_is_plong(self):
+        self.assertEqual(convert(7), "Plong")
+
+    def test_the_sound_for_6_is_pling_as_it_has_a_factor_3(self):
+        self.assertEqual(convert(6), "Pling")
+
+    def test_2_to_the_power_3_does_not_make_a_raindrop_sound_as_3_is_the_exponent_not_the_base(
+        self
+    ):
+        self.assertEqual(convert(8), "8")
+
+    def test_the_sound_for_9_is_pling_as_it_has_a_factor_3(self):
+        self.assertEqual(convert(9), "Pling")
+
+    def test_the_sound_for_10_is_plang_as_it_has_a_factor_5(self):
+        self.assertEqual(convert(10), "Plang")
+
+    def test_the_sound_for_14_is_plong_as_it_has_a_factor_of_7(self):
+        self.assertEqual(convert(14), "Plong")
+
+    def test_the_sound_for_15_is_pling_plang_as_it_has_factors_3_and_5(self):
+        self.assertEqual(convert(15), "PlingPlang")
+
+    def test_the_sound_for_21_is_pling_plong_as_it_has_factors_3_and_7(self):
+        self.assertEqual(convert(21), "PlingPlong")
+
+    def test_the_sound_for_25_is_plang_as_it_has_a_factor_5(self):
+        self.assertEqual(convert(25), "Plang")
+
+    def test_the_sound_for_27_is_pling_as_it_has_a_factor_3(self):
+        self.assertEqual(convert(27), "Pling")
+
+    def test_the_sound_for_35_is_plang_plong_as_it_has_factors_5_and_7(self):
+        self.assertEqual(convert(35), "PlangPlong")
+
+    def test_the_sound_for_49_is_plong_as_it_has_a_factor_7(self):
+        self.assertEqual(convert(49), "Plong")
+
+    def test_the_sound_for_52_is_52(self):
+        self.assertEqual(convert(52), "52")
+
+    def test_the_sound_for_105_is_pling_plang_plong_as_it_has_factors_3_5_and_7(self):
+        self.assertEqual(convert(105), "PlingPlangPlong")
+
+    def test_the_sound_for_3125_is_plang_as_it_has_a_factor_5(self):
+        self.assertEqual(convert(3125), "Plang")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/python/robot-name/README.md b/python/robot-name/README.md
new file mode 100644
index 0000000..e8e1c95
--- /dev/null
+++ b/python/robot-name/README.md
@@ -0,0 +1,63 @@
+# Robot Name
+
+Manage robot factory settings.
+
+When robots come off the factory floor, they have no name.
+
+The first time you boot them up, a random name is generated in the format
+of two uppercase letters followed by three digits, such as RX837 or BC811.
+
+Every once in a while we need to reset a robot to its factory settings,
+which means that their name gets wiped. The next time you ask, it will
+respond with a new random name.
+
+The names must be random: they should not follow a predictable sequence.
+Random names means a risk of collisions. Your solution must ensure that
+every existing robot has a unique name.
+
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should 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. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest robot_name_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest robot_name_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/robot-name` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+A debugging session with Paul Blackwell at gSchool. [http://gschool.it](http://gschool.it)
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/python/robot-name/robot_name.py b/python/robot-name/robot_name.py
new file mode 100644
index 0000000..82ec001
--- /dev/null
+++ b/python/robot-name/robot_name.py
@@ -0,0 +1,22 @@
+import random
+import string
+
+
+class Robot:
+    Names = set()
+
+    def __init__(self):
+        self.name = ''
+        self._gen_name()
+
+    def reset(self):
+        self.name = ''
+        self._gen_name()
+
+    def _gen_name(self):
+        while not self.name or self.name in Robot.Names:
+            self.name = ''.join(
+                    random.choices(string.ascii_uppercase, k=2) +
+                    random.choices(string.digits, k=3)
+                    )
+        Robot.Names.add(self.name)
diff --git a/python/robot-name/robot_name_test.py b/python/robot-name/robot_name_test.py
new file mode 100644
index 0000000..f023b05
--- /dev/null
+++ b/python/robot-name/robot_name_test.py
@@ -0,0 +1,51 @@
+import unittest
+import random
+
+from robot_name import Robot
+
+
+class RobotNameTest(unittest.TestCase):
+    # assertRegex() alias to adress DeprecationWarning
+    # assertRegexpMatches got renamed in version 3.2
+    if not hasattr(unittest.TestCase, "assertRegex"):
+        assertRegex = unittest.TestCase.assertRegexpMatches
+
+    name_re = r'^[A-Z]{2}\d{3}$'
+
+    def test_has_name(self):
+        self.assertRegex(Robot().name, self.name_re)
+
+    def test_name_sticks(self):
+        robot = Robot()
+        robot.name
+        self.assertEqual(robot.name, robot.name)
+
+    def test_different_robots_have_different_names(self):
+        self.assertNotEqual(
+            Robot().name,
+            Robot().name
+        )
+
+    def test_reset_name(self):
+        # Set a seed
+        seed = "Totally random."
+
+        # Initialize RNG using the seed
+        random.seed(seed)
+
+        # Call the generator
+        robot = Robot()
+        name = robot.name
+
+        # Reinitialize RNG using seed
+        random.seed(seed)
+
+        # Call the generator again
+        robot.reset()
+        name2 = robot.name
+        self.assertNotEqual(name, name2)
+        self.assertRegex(name2, self.name_re)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/python/two-fer/README.md b/python/two-fer/README.md
new file mode 100644
index 0000000..5528964
--- /dev/null
+++ b/python/two-fer/README.md
@@ -0,0 +1,72 @@
+# Two Fer
+
+`Two-fer` or `2-fer` is short for two for one. One for you and one for me.
+
+Given a name, return a string with the message:
+
+```text
+One for X, one for me.
+```
+
+Where X is the given name.
+
+However, if the name is missing, return the string:
+
+```text
+One for you, one for me.
+```
+
+Here are some examples:
+
+|Name    |String to return
+|:-------|:------------------
+|Alice   |One for Alice, one for me.
+|Bob     |One for Bob, one for me.
+|        |One for you, one for me.
+|Zaphod  |One for Zaphod, one for me.
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should 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. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest two_fer_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest two_fer_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/two-fer` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+[https://github.com/exercism/problem-specifications/issues/757](https://github.com/exercism/problem-specifications/issues/757)
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/python/two-fer/two_fer.py b/python/two-fer/two_fer.py
new file mode 100644
index 0000000..425d236
--- /dev/null
+++ b/python/two-fer/two_fer.py
@@ -0,0 +1,2 @@
+def two_fer(name='you'):
+    return f'One for {name}, one for me.'
diff --git a/python/two-fer/two_fer_test.py b/python/two-fer/two_fer_test.py
new file mode 100644
index 0000000..58f7386
--- /dev/null
+++ b/python/two-fer/two_fer_test.py
@@ -0,0 +1,20 @@
+import unittest
+
+from two_fer import two_fer
+
+# Tests adapted from `problem-specifications//canonical-data.json` @ v1.2.0
+
+
+class TwoFerTest(unittest.TestCase):
+    def test_no_name_given(self):
+        self.assertEqual(two_fer(), "One for you, one for me.")
+
+    def test_a_name_given(self):
+        self.assertEqual(two_fer("Alice"), "One for Alice, one for me.")
+
+    def test_another_name_given(self):
+        self.assertEqual(two_fer("Bob"), "One for Bob, one for me.")
+
+
+if __name__ == "__main__":
+    unittest.main()
-- 
GitLab