\n",
@@ -498,7 +640,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "31",
+ "id": "34",
"metadata": {
"tags": []
},
@@ -507,15 +649,23 @@
"%%ipytest\n",
"\n",
"def solution_range_of_nums(start: int, end: int) -> list[int]:\n",
- " \"\"\"\n",
- " Write your solution here\n",
+ " \"\"\"Creates a list of consecutive integers from start to end, inclusive of both boundaries.\n",
+ " \n",
+ " The sequence can be either increasing or decreasing depending on the input values.\n",
+ "\n",
+ " Args:\n",
+ " start: The first number in the range\n",
+ " end: The last number in the range\n",
+ "\n",
+ " Returns:\n",
+ " - A list of integers containing all numbers from start to end (inclusive), in the correct order\n",
" \"\"\"\n",
" return"
]
},
{
"cell_type": "markdown",
- "id": "32",
+ "id": "35",
"metadata": {
"tags": []
},
@@ -534,7 +684,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "33",
+ "id": "36",
"metadata": {
"tags": []
},
@@ -544,16 +694,24 @@
"\n",
"import math\n",
"\n",
- "def solution_sqrt_of_nums(numbers: list) -> list:\n",
- " \"\"\"\n",
- " Write your solution here\n",
+ "def solution_sqrt_of_nums(numbers: list[int]) -> list[int]:\n",
+ " \"\"\"Calculates the square root of each number in the input list.\n",
+ "\n",
+ " Uses `math.sqrt` to compute square roots. Numbers that don't have a real square root\n",
+ " (negative numbers) are skipped in the output.\n",
+ "\n",
+ " Args:\n",
+ " numbers: A list of integers to process\n",
+ "\n",
+ " Returns:\n",
+ " - A list of floats containing the square root of each valid number from the input list\n",
" \"\"\"\n",
" return"
]
},
{
"cell_type": "markdown",
- "id": "34",
+ "id": "37",
"metadata": {},
"source": [
"#### 4. Write a Python program that takes an integer and divides it by 2 until the result is no longer an even number"
@@ -561,7 +719,7 @@
},
{
"cell_type": "markdown",
- "id": "35",
+ "id": "38",
"metadata": {},
"source": [
"
\n",
@@ -573,7 +731,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "36",
+ "id": "39",
"metadata": {
"tags": []
},
@@ -581,16 +739,24 @@
"source": [
"%%ipytest\n",
"\n",
- "def solution_divide_until(num: int) -> int:\n",
- " \"\"\"\n",
- " Write your solution here\n",
+ "def solution_divide_until(number: int) -> int:\n",
+ " \"\"\"Repeatedly divides a number by 2 until the result becomes odd.\n",
+ "\n",
+ " Starting with the input number, performs integer division by 2 repeatedly\n",
+ " until reaching a number that cannot be evenly divided by 2 (an odd number).\n",
+ "\n",
+ " Args:\n",
+ " num: The starting integer to be divided\n",
+ "\n",
+ " Returns:\n",
+ " - The first odd number encountered in the division sequence\n",
" \"\"\"\n",
" return"
]
},
{
"cell_type": "markdown",
- "id": "37",
+ "id": "40",
"metadata": {},
"source": [
"---"
@@ -598,42 +764,7 @@
},
{
"cell_type": "markdown",
- "id": "38",
- "metadata": {
- "tags": []
- },
- "source": [
- "## Nested loops"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "39",
- "metadata": {
- "jp-MarkdownHeadingCollapsed": true,
- "tags": []
- },
- "source": [
- "You can put loops inside of other loops (of any kind). Just be careful to respect the indentation:\n",
- "\n",
- "```python\n",
- "for n in range(1, 4):\n",
- " for m in range(4, 7):\n",
- " print(\"n = \", n, \" and j = \", m)\n",
- "```\n",
- "\n",
- "The outer loop over `n` goes from 1 to 3. For each iteration, a new inner loop over `m` is started from 4 to 6. You will get **9 lines of output**, as the two range objects contain exactly 3 elements each.\n",
- "\n",
- "
\n",
- "
Important
\n",
- " Nesting loops can have dramatic consequences on your program's performance.
\n",
- " The body of the loop above repeats $n \\times m$ times. If $n$, $m$, or both are large numbers, your program might take a while to finish.\n",
- ""
- ]
- },
- {
- "cell_type": "markdown",
- "id": "40",
+ "id": "41",
"metadata": {
"tags": []
},
@@ -643,22 +774,21 @@
},
{
"cell_type": "markdown",
- "id": "41",
+ "id": "42",
"metadata": {},
"source": [
- "There are **4 ways** in which you can alter the normal execution of a loop:\n",
+ "There are **3 ways** in which you can alter the normal execution of a loop:\n",
"\n",
"1. With an `if` statement inside a `for`/`while` loop\n",
- "2. With the `break` keyword: the loop stops **immediately**\n",
- "3. With the `continue` keyword: the statements **after** the keyword are skipped and the next iteration is started\n",
- "4. With the `else` clause after a `for`/`while` body: the `else` statement(s) are run **only** if no `break` statement is encountered in the loop body\n",
+ "2. With the `break` or `continue` keywords\n",
+ "3. With the `else` clause after a `for`/`while` body: the `else` statement(s) are run **only** if no `break` statement is encountered in the loop body\n",
"\n",
- "Let's see an example for each of these"
+ "Let's see each of these in more detail."
]
},
{
"cell_type": "markdown",
- "id": "42",
+ "id": "43",
"metadata": {},
"source": [
"### `if` statement inside `for`/`while`\n",
@@ -669,7 +799,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "43",
+ "id": "44",
"metadata": {
"tags": []
},
@@ -688,94 +818,355 @@
},
{
"cell_type": "markdown",
- "id": "44",
+ "id": "45",
"metadata": {},
"source": [
- "### The `break` keyword\n",
+ "#### Exercise: conditionals inside loops\n",
+ "\n",
+ "Complete the function `solution_filter_by_position` below that filters a list of integers, keeping only numbers larger than their position in the list (**1-based index**).\n",
+ "\n",
+ "The filtered numbers should:\n",
+ "\n",
+ "- **Not** contain duplicates\n",
+ "- Be in **ascending** order\n",
"\n",
- "In this example, the `while` loop will continue until `i` is equal to 5. At that point, the `break` statement is executed, causing the loop to terminate prematurely."
+ "You must use an `if-else` inside a loop.\n",
+ "\n",
+ "Example: `[1, 3, 0, 2]` should return `[3]` because:\n",
+ "- 1 is not greater than position 1\n",
+ "- 3 is greater than position 2\n",
+ "- 0 is not greater than position 3\n",
+ "- 2 is not greater than position 4"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "45",
- "metadata": {
- "tags": []
- },
+ "id": "46",
+ "metadata": {},
"outputs": [],
"source": [
- "i = 0\n",
- "while i < 10:\n",
- " i += 1\n",
- " if i == 5:\n",
- " break\n",
- " print(i)\n",
+ "%%ipytest\n",
"\n",
- "print(\"Loop terminated with break\")"
+ "def solution_filter_by_position(numbers: list[int]) -> list[int]:\n",
+ " \"\"\"Filters numbers that are larger than their position in the list.\n",
+ "\n",
+ " Args:\n",
+ " numbers: List of integers to filter\n",
+ "\n",
+ " Returns:\n",
+ " - A new list containing only the numbers that are greater than\n",
+ " their 1-based position in the input list\n",
+ " \"\"\"\n",
+ " return"
]
},
{
"cell_type": "markdown",
- "id": "46",
+ "id": "47",
"metadata": {},
"source": [
- "### The `continue` keyword\n",
- "\n",
- "In this example, the `for` loop will iterate over the numbers from 0 to 9. However, when `i` is **even**, the `continue` statement is executed, causing the loop to skip the rest of the statements in the loop and move on to the next iteration"
+ "---"
]
},
{
- "cell_type": "code",
- "execution_count": null,
- "id": "47",
- "metadata": {
- "tags": []
- },
- "outputs": [],
+ "cell_type": "markdown",
+ "id": "48",
+ "metadata": {},
"source": [
- "for i in range(10):\n",
- " if i % 2 == 0:\n",
- " continue\n",
- " print(i)\n",
+ "### `break` and `continue`\n",
"\n",
- "print(\"Loop terminated normally\")"
+ "Python provides these two special keywords to alter the normal flow of loops in two ways that might sound very similar.\n",
+ "However, they serve very different purposes."
]
},
{
"cell_type": "markdown",
- "id": "48",
+ "id": "49",
"metadata": {},
"source": [
- "### `else` after a `for`/`while`\n",
+ "#### The `break` keyword\n",
"\n",
- "Here the `for` loop iterates over a list of numbers. If the current `num` is equal to 4, a print statement and then `break` are executed.\n",
+ "The `break` statement immediately terminates the loop it's in, skipping any remaining iterations.\n",
+ "It's particularly useful when:\n",
"\n",
- "If we complete the loop without finding 4, then the `else` block is executed and a message is printed indicating that 4 was not found in the list."
+ "- You've found what you're looking for\n",
+ "- You've encountered an error condition\n",
+ "- You want to exit early based on some condition\n",
+ "\n",
+ "##### Examples of using `break`\n",
+ "\n",
+ "1. **Finding an element in a list:**\n",
+ "```python\n",
+ "numbers = [4, 7, 2, 9, 1, 5]\n",
+ "target = 9\n",
+ "\n",
+ "for num in numbers:\n",
+ " if num == target:\n",
+ " print(f\"Found {target}!\")\n",
+ " break\n",
+ " print(f\"Checking {num}...\")\n",
+ "```\n",
+ "\n",
+ "2. **Input validation with `while` loop:**\n",
+ "```python\n",
+ "while True:\n",
+ " password = input(\"Enter your password: \")\n",
+ " if len(password) >= 8:\n",
+ " print(\"Password accepted!\")\n",
+ " break\n",
+ " print(\"Password must be at least 8 characters.\")\n",
+ "```\n",
+ "\n",
+ "3. **Early exit from nested loops:**\n",
+ "```python\n",
+ "matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]\n",
+ "target = 5\n",
+ "\n",
+ "found = False\n",
+ "for i, row in enumerate(matrix):\n",
+ " for j, value in enumerate(row):\n",
+ " if value == target:\n",
+ " print(f\"Found {target} at position ({i}, {j})\")\n",
+ " found = True\n",
+ " break\n",
+ " if found: # Break outer loop\n",
+ " break\n",
+ "```\n",
+ "\n",
+ "Here's an example you can play with:"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "49",
+ "id": "50",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
- "numbers = [1, 3, 5, 7, 9]\n",
- "\n",
- "for num in numbers:\n",
- " if num == 4:\n",
- " print(\"Found 4 - breaking loop\")\n",
+ "i = 0\n",
+ "while i < 10:\n",
+ " i += 1\n",
+ " if i == 5:\n",
" break\n",
- "else:\n",
+ " print(i)\n",
+ "\n",
+ "print(\"Loop terminated with break\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "51",
+ "metadata": {},
+ "source": [
+ "#### The `continue` keyword\n",
+ "\n",
+ "The `continue` statement skips the rest of the current iteration and moves to the next one.\n",
+ "It's useful when:\n",
+ "\n",
+ "- You want to skip certain elements\n",
+ "- You want to avoid nested `if` statements\n",
+ "- You have some cases you **don't want** to process\n",
+ "\n",
+ "\n",
+ "##### Examples of `continue`\n",
+ "\n",
+ "1. **Processing only specific items:**\n",
+ "```python\n",
+ "numbers = [1, -2, 3, -4, 5, -6]\n",
+ "\n",
+ "for num in numbers:\n",
+ " if num < 0:\n",
+ " continue\n",
+ " print(f\"Processing positive number: {num}\")\n",
+ "```\n",
+ "\n",
+ "2. **Skipping empty or invalid items:**\n",
+ "```python\n",
+ "data = [\"apple\", \"\", \"banana\", None, \"cherry\"]\n",
+ "\n",
+ "for item in data:\n",
+ " if not item: # Skip empty or None values\n",
+ " continue\n",
+ " print(f\"Processing {item.upper()}\")\n",
+ "```\n",
+ "\n",
+ "3. **Complex filtering conditions:**\n",
+ "```python\n",
+ "def process_user_better(user):\n",
+ " if user['age'] < 18:\n",
+ " continue\n",
+ " if not user['email']:\n",
+ " continue\n",
+ " if user['status'] != 'active':\n",
+ " continue\n",
+ " \n",
+ " print(f\"Processing user: {user['name']}\")\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "52",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "for i in range(10):\n",
+ " if i % 2 == 0:\n",
+ " continue\n",
+ " print(i)\n",
+ "\n",
+ "print(\"Loop terminated normally\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "53",
+ "metadata": {},
+ "source": [
+ "#### Which one should I use?\n",
+ "\n",
+ "Use `break` when you:\n",
+ " - Want to completely exit a loop\n",
+ " - Have found what you're looking for\n",
+ " - Need to handle error conditions\n",
+ "\n",
+ "Use `continue` when you:\n",
+ " - Want to skip certain elements\n",
+ " - Need to avoid nested conditional code\n",
+ " - Have cases you want to ignore\n",
+ "\n",
+ "
\n",
+ "💡 Hint: Both break
and continue
can make code harder to read if overused. Sometimes a simple if
statement or restructuring your loop might be clearer. Use these statements when they genuinely simplify your code.\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "54",
+ "metadata": {},
+ "source": [
+ "#### Common patterns\n",
+ "\n",
+ "##### Pattern 1: Loop with early exit\n",
+ "\n",
+ "```python\n",
+ "for item in items:\n",
+ " if not validate(item):\n",
+ " break\n",
+ " process(item)\n",
+ "```\n",
+ "\n",
+ "##### Pattern 2: Skip invalid items\n",
+ "\n",
+ "```python\n",
+ "for item in items:\n",
+ " if not validate(item):\n",
+ " continue\n",
+ " process(item)\n",
+ "```\n",
+ "\n",
+ "##### Pattern 3: Process until condition\n",
+ "\n",
+ "```python\n",
+ "while True:\n",
+ " data = get_data()\n",
+ " if not data:\n",
+ " break\n",
+ " process(data)\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "55",
+ "metadata": {},
+ "source": [
+ "
\n",
+ "Remember: Both break
and continue
only affect the innermost loop they appear in.\n",
+ "For nested loops, you might need additional variables or logic to control outer loops.\n",
+ "
\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "56",
+ "metadata": {},
+ "source": [
+ "#### Exercise: breaking out of loops\n",
+ "\n",
+ "Complete the function `solution_find_even_multiple_three` below that searches through a list of numbers and returns the first even number that's also a multiple of 3.\n",
+ "If no such number exists, return `None`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "57",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%ipytest\n",
+ "\n",
+ "def solution_find_even_multiple_three(numbers: list[int]) -> int:\n",
+ " \"\"\"Finds the first even number that's also a multiple of 3.\n",
+ "\n",
+ " Args:\n",
+ " numbers: List of integers to search through\n",
+ "\n",
+ " Returns:\n",
+ " - The first number that is both even and divisible by 3, or None if no such number exists\n",
+ " \"\"\"\n",
+ " return"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "58",
+ "metadata": {},
+ "source": [
+ "---"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "59",
+ "metadata": {},
+ "source": [
+ "### `else` after a `for`/`while`\n",
+ "\n",
+ "Here the `for` loop iterates over a list of numbers. If the current `num` is equal to 4, a print statement and then `break` are executed.\n",
+ "\n",
+ "If we complete the loop without finding 4, then the `else` block is executed and a message is printed indicating that 4 was not found in the list."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "60",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "numbers = [1, 3, 5, 7, 9]\n",
+ "\n",
+ "for num in numbers:\n",
+ " if num == 4:\n",
+ " print(\"Found 4 - breaking loop\")\n",
+ " break\n",
+ "else:\n",
" print(\"4 not found in list\")"
]
},
{
"cell_type": "markdown",
- "id": "50",
+ "id": "61",
"metadata": {},
"source": [
"Let's see what happens if we change the list to `[1, 2, 3, 4]`:"
@@ -784,7 +1175,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "51",
+ "id": "62",
"metadata": {
"tags": []
},
@@ -802,7 +1193,122 @@
},
{
"cell_type": "markdown",
- "id": "52",
+ "id": "63",
+ "metadata": {},
+ "source": [
+ "#### Exercise: using `else` in loops\n",
+ "\n",
+ "Complete the function `solution_is_pure_number` below that checks if a string is a \"pure\" number (that is, it contains **only** digits).\n",
+ "The function should return `True` if the string is pure digits, `False` otherwise.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%ipytest\n",
+ "\n",
+ "def solution_is_pure_number(text: str) -> bool:\n",
+ " \"\"\"Checks if a string contains only digits.\n",
+ "\n",
+ " Uses a for-else structure to verify that every character is a digit.\n",
+ "\n",
+ " Args:\n",
+ " text: String to check\n",
+ "\n",
+ " Returns:\n",
+ " - A boolean value: True if the string contains only digits, False otherwise. An empty string returns True.\n",
+ " \"\"\"\n",
+ " return"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "65",
+ "metadata": {},
+ "source": [
+ "---"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "66",
+ "metadata": {},
+ "source": [
+ "## Nested loops"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "67",
+ "metadata": {},
+ "source": [
+ "You can put loops inside of other loops (of any kind). Just be careful to respect the indentation:\n",
+ "\n",
+ "```python\n",
+ "for n in range(1, 4):\n",
+ " for m in range(4, 7):\n",
+ " print(\"n = \", n, \" and j = \", m)\n",
+ "```\n",
+ "\n",
+ "The outer loop over `n` goes from 1 to 3. For each iteration, a new inner loop over `m` is started from 4 to 6. You will get **9 lines of output**, as the two range objects contain exactly 3 elements each.\n",
+ "\n",
+ "
\n",
+ "
Important: Performance Considerations
\n",
+ " Nesting loops can have dramatic consequences on your program's performance.
\n",
+ " The body of the loop above repeats $n \\times m$ times. If $n$, $m$, or both are large numbers, your program might take a while to finish.\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "68",
+ "metadata": {},
+ "source": [
+ "### Understanding the performance impact\n",
+ "\n",
+ "How much time do you (roughly) need if you are nesting multiple loops?\n",
+ "\n",
+ "- **Single loop**: the time required is proportional to the number of elements ($n$) we are iterating over.\n",
+ "- **Two nested loops**: the time required is proportional to $n^2$.\n",
+ "- **Three nested loops**: the time requires is proportional to $n^3$.\n",
+ "\n",
+ "For example, with $n=1000$:\n",
+ "\n",
+ "- Single loop: 1,000 iterations\n",
+ "- Two nested loops: 1,000,000 iterations\n",
+ "- Three nested loops: 1,000,000,000 iterations\n",
+ "\n",
+ "In the last case, with a billion iterations, even the fastest computer will show some slowdown.\n",
+ "In these cases, a slowdown due to multiply-nested loops is almost always undesirable, and we should try a better way."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "69",
+ "metadata": {},
+ "source": [
+ "### Tips to improve performance\n",
+ "\n",
+ "These tips are most likely out of scope for an introductory tutorial, but for the sake of completeness, you can investigate if any of these is applicable to your case:\n",
+ "\n",
+ "1. Avoid unnecessary nesting\n",
+ "\n",
+ "2. Pre-compute values (if some computation is expensive)\n",
+ "\n",
+ "3. Think about using a different data structure (e.g. a `set()` instead of a `list()`)\n",
+ "\n",
+ "4. Use built-in functions (or libraries) instead of writing a solution from scratch\n",
+ "\n",
+ "5. Stop iterating with `break` if possible, thereby reducing the number of iterations to be performed"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "70",
"metadata": {},
"source": [
"## Quiz on loops"
@@ -811,7 +1317,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "53",
+ "id": "71",
"metadata": {},
"outputs": [],
"source": [
@@ -821,7 +1327,7 @@
},
{
"cell_type": "markdown",
- "id": "54",
+ "id": "72",
"metadata": {
"tags": []
},
@@ -831,7 +1337,7 @@
},
{
"cell_type": "markdown",
- "id": "55",
+ "id": "73",
"metadata": {},
"source": [
"Another way of controlling the flow of a program is by **catching exceptions**.\n",
@@ -852,17 +1358,17 @@
},
{
"cell_type": "markdown",
- "id": "56",
+ "id": "74",
"metadata": {
"tags": []
},
"source": [
- "## The `try-except` block"
+ "### The `try-except` block"
]
},
{
"cell_type": "markdown",
- "id": "57",
+ "id": "75",
"metadata": {},
"source": [
"When you can predict if a certain exception might occur, it's **always a good programming practice** to write what the program should do in that case\n",
@@ -873,7 +1379,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "58",
+ "id": "76",
"metadata": {
"tags": []
},
@@ -891,7 +1397,7 @@
},
{
"cell_type": "markdown",
- "id": "59",
+ "id": "77",
"metadata": {},
"source": [
"You can handle **multiple exceptions** at the same time"
@@ -900,7 +1406,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "60",
+ "id": "78",
"metadata": {
"tags": []
},
@@ -917,7 +1423,7 @@
},
{
"cell_type": "markdown",
- "id": "61",
+ "id": "79",
"metadata": {
"tags": []
},
@@ -941,7 +1447,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "62",
+ "id": "80",
"metadata": {
"tags": []
},
@@ -960,7 +1466,7 @@
},
{
"cell_type": "markdown",
- "id": "63",
+ "id": "81",
"metadata": {},
"source": [
"- We try to open a file called `README.txt`. A `FileNotFoundError` will be raised if it's not found\n",
@@ -972,7 +1478,116 @@
},
{
"cell_type": "markdown",
- "id": "64",
+ "id": "82",
+ "metadata": {},
+ "source": [
+ "### The `raise` statement"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "83",
+ "metadata": {},
+ "source": [
+ "What if you want to *explicitly* raise an exception?\n",
+ "The preliminary question is \"why should I need raising an exception?\" There are several situations where this is appropriate.\n",
+ "Let's see a few of them.\n",
+ "\n",
+ "1. **Triggering a custom behavior**\n",
+ "\n",
+ "```python\n",
+ "def divide_numbers(a, b):\n",
+ " if b == 0:\n",
+ " raise ValueError(\"Division by zero is not allowed\")\n",
+ " return a / b\n",
+ "\n",
+ "try:\n",
+ " result = divide_numbers(10, 0)\n",
+ "except ValueError as e:\n",
+ " print(f\"Error: {e}\") # Will print: \"Error: Division by zero is not allowed\"\n",
+ "```\n",
+ "\n",
+ "In this example, we explicitly raise a `ValueError` when we detect an invalid operation, instead of relying on Python's default behavior, which would raise a `ZeroDivisionError`.\n",
+ "The main reason to do this is to provide a more meaningful error message, or perform some other actions if a particular exception occurs.\n",
+ "\n",
+ "2. **Raising custom exceptions**\n",
+ "\n",
+ "We can create custom exceptions by defining a class.\n",
+ "We'll see how to work with classes and objects on the last day of the tutorial, but now just remember that's another\n",
+ "Here's an example of how to define a custom exception:\n",
+ "\n",
+ "```python\n",
+ "class InsufficientFundsError(Exception):\n",
+ " \"\"\"Custom exception for banking operations\"\"\"\n",
+ " pass\n",
+ "```\n",
+ "\n",
+ "Don't worry about the syntax: you will learn everything you need when we'll be dealing with object-oriented programming.\n",
+ "\n",
+ "3. **Chaining exceptions**\n",
+ "\n",
+ "Say the we are dealing with some configuration file that we want to open.\n",
+ "We surely need to verify that the file exists, for example:\n",
+ "\n",
+ "```python\n",
+ "def open_file(filename):\n",
+ " try:\n",
+ " # do something like trying to access the file\n",
+ " except FileNotFoundError as error:\n",
+ " raise ValueError(f\"File {} was not found\") from error\n",
+ "\n",
+ "try:\n",
+ " config_file = open_file(\"config.json\")\n",
+ "except ValueError:\n",
+ " print(f\"Error: {e}\")\n",
+ " print(f\"Original error: {e.__cause__}\")\n",
+ "```\n",
+ "\n",
+ "Here we are using a variation of `raise` that is `raise from`.\n",
+ "What are the benefits?\n",
+ "\n",
+ "- It preserves the original exception as the cause (`__cause__`)\n",
+ "- It creates a clear chain of exceptions, showing how one error led to another\n",
+ "- It helps in debugging by maintaining the full error context\n",
+ "\n",
+ "4. **Re-raising exceptions**\n",
+ "\n",
+ "Sometimes we do not need custom exceptions, but it might be useful to perform some actions *before* Python raises an uncaught exception.\n",
+ "For example, logging the error somewhere if our program is not running interactively (because maybe it's processing our data overnight).\n",
+ "\n",
+ "```python\n",
+ "def process_data(data):\n",
+ " try:\n",
+ " # Some processing ...\n",
+ " result = data['key'] / 0\n",
+ " except Exception as e:\n",
+ " print(\"Logging error...\")\n",
+ " raise # Re-raises the same exception\n",
+ "```\n",
+ "\n",
+ "Simply using `raise` without arguments is useful when:\n",
+ "\n",
+ "- We want to preserve the original traceback (i.e., the full *error context*)\n",
+ "- We want to do something if the error occurs, but we still want it to propagate up and be caught by the interpreter (or another `try-except` block) "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "84",
+ "metadata": {},
+ "source": [
+ "A few advice to keep in mind when using `raise`:\n",
+ "\n",
+ "1. Always raise specific exceptions rather than generic ones\n",
+ "2. Provide clear and detailed error messages\n",
+ "3. Use custom exceptions for specific errors\n",
+ "4. Use `raise from` when converting between exception types to maintain context\n",
+ "5. Only catch exceptions you can handle meaningfully"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "85",
"metadata": {
"tags": []
},
@@ -983,38 +1598,38 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "65",
+ "id": "86",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
- "%reload_ext tutorial.tests.testsuite\n",
- "\n",
- "import pathlib"
+ "%reload_ext tutorial.tests.testsuite"
]
},
{
"cell_type": "markdown",
- "id": "66",
+ "id": "87",
"metadata": {
"jp-MarkdownHeadingCollapsed": true,
"tags": []
},
"source": [
- "## Find the factors 🌶️"
+ "## Exercise 1: Find the factors\n",
+ "\n",
+ "**Difficulty:** 🌶️"
]
},
{
"cell_type": "markdown",
- "id": "67",
+ "id": "88",
"metadata": {},
"source": [
"A factor of a positive integer `n` is any positive integer less than or equal to `n` that divides `n` with no remainder.\n",
"\n",
"
\n",
"
Question
\n",
- " Complete the Python code below. Given an integer n
, return the list of all integers m
<= n
that are factors of n
.\n",
+ " Given an integer n
, return the list of all integers m <= n
that are factors of n
.\n",
"\n",
"\n",
"
\n",
@@ -1026,7 +1641,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "68",
+ "id": "89",
"metadata": {
"tags": []
},
@@ -1035,34 +1650,42 @@
"%%ipytest\n",
"\n",
"def solution_find_factors(n: int) -> list[int]:\n",
+ " \"\"\"Finds all positive factors of a given number.\n",
+ "\n",
+ " A factor is any positive integer that divides n without leaving a remainder.\n",
+ " The function checks all integers from 1 to n (inclusive) and collects those\n",
+ " that are factors of n.\n",
+ "\n",
+ " Args:\n",
+ " n: A positive integer whose factors are to be found\n",
+ "\n",
+ " Returns:\n",
+ " - A sorted list of all positive integers that divide n without remainder.\n",
+ " For n <= 0, returns an empty list since factors are defined only for positive integers.\n",
" \"\"\"\n",
- " Write your solution here\n",
- " \"\"\"\n",
- " pass"
+ " return"
]
},
{
"cell_type": "markdown",
- "id": "69",
+ "id": "90",
"metadata": {
"jp-MarkdownHeadingCollapsed": true,
"tags": []
},
"source": [
- "## Find the pair 🌶️"
+ "## Exercise 2: Find the pair\n",
+ "\n",
+ "**Difficulty:** 🌶️"
]
},
{
"cell_type": "markdown",
- "id": "70",
+ "id": "91",
"metadata": {},
"source": [
- "Given a list of integers, your task is to complete the code below that finds the pair of numbers in the list that add up to `2020`. The list of numbers is already available as the variable `nums`.\n",
- "\n",
- "
\n",
- "
Question
\n",
- " What do you get if you multiply them together?\n",
- "\n",
+ "Given a list of integers, write a function that finds the **first** pair of numbers in the list that add up to `2020` and return **their product**.\n",
+ "The list of numbers is already available as the variable `nums`.\n",
"\n",
"
\n",
"
Hint
\n",
@@ -1072,7 +1695,7 @@
},
{
"cell_type": "markdown",
- "id": "71",
+ "id": "92",
"metadata": {
"tags": []
},
@@ -1083,7 +1706,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "72",
+ "id": "93",
"metadata": {
"tags": []
},
@@ -1092,15 +1715,25 @@
"%%ipytest\n",
"\n",
"def solution_find_pair(nums: list[int]) -> int:\n",
+ " \"\"\"Finds the product of two numbers from the input list that sum to 2020.\n",
+ "\n",
+ " Searches through all possible pairs of numbers in the input list to find\n",
+ " two different numbers that add up to 2020. When found, returns their product.\n",
+ "\n",
+ " Args:\n",
+ " nums: A list of integers to search through\n",
+ "\n",
+ " Returns:\n",
+ " - The product of the two numbers that sum to 2020.\n",
+ " If no such pair exists, returns None.\n",
+ " If multiple pairs exist, returns the product of the first pair found.\n",
" \"\"\"\n",
- " Write your solution here\n",
- " \"\"\"\n",
- " pass"
+ " return"
]
},
{
"cell_type": "markdown",
- "id": "73",
+ "id": "94",
"metadata": {
"tags": []
},
@@ -1110,7 +1743,7 @@
},
{
"cell_type": "markdown",
- "id": "74",
+ "id": "95",
"metadata": {},
"source": [
"
\n",
@@ -1120,14 +1753,14 @@
"\n",
"
\n",
"
Hint
\n",
- " Too many nested loops can worsen a lot your code's performance\n",
+ " Too many nested loops can worsen significantly your code's performance.\n",
""
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "75",
+ "id": "96",
"metadata": {
"tags": []
},
@@ -1136,26 +1769,39 @@
"%%ipytest\n",
"\n",
"def solution_find_triplet(nums: list[int]) -> int:\n",
+ " \"\"\"Finds the product of three numbers from the input list that sum to 2020.\n",
+ "\n",
+ " Searches through all possible combinations of three different numbers in the \n",
+ " input list to find three numbers that add up to 2020. When found, returns \n",
+ " their product.\n",
+ "\n",
+ " Args:\n",
+ " nums: A list of integers to search through\n",
+ "\n",
+ " Returns:\n",
+ " - The product of the three numbers that sum to 2020.\n",
+ " If no such triplet exists, returns None.\n",
+ " If multiple triplets exist, returns the product of the first triplet found.\n",
" \"\"\"\n",
- " Write your solution here\n",
- " \"\"\"\n",
- " pass"
+ " return"
]
},
{
"cell_type": "markdown",
- "id": "76",
+ "id": "97",
"metadata": {
"jp-MarkdownHeadingCollapsed": true,
"tags": []
},
"source": [
- "## Cats with hats 🌶️🌶️"
+ "## Exercise 3: Cats with hats\n",
+ "\n",
+ "**Difficulty:** 🌶️🌶️"
]
},
{
"cell_type": "markdown",
- "id": "77",
+ "id": "98",
"metadata": {},
"source": [
"You have 100 cats.\n",
@@ -1175,14 +1821,14 @@
"\n",
"
\n",
"
Hint
\n",
- " You can approach this problem with either lists ([]
) or dictionaries (key-value pairs).\n",
+ " You can approach this problem with either lists or dictionaries.\n",
""
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "78",
+ "id": "99",
"metadata": {
"tags": []
},
@@ -1191,196 +1837,83 @@
"%%ipytest\n",
"\n",
"def solution_cats_with_hats() -> int:\n",
+ " \"\"\"Simulates putting hats on cats in a circular arrangement through multiple rounds.\n",
+ "\n",
+ " Simulates 100 rounds where each round visits cats at different intervals:\n",
+ " - Round 1: visits every cat (1, 2, 3, ...)\n",
+ " - Round 2: visits every 2nd cat (2, 4, 6, ...)\n",
+ " - Round 3: visits every 3rd cat (3, 6, 9, ...)\n",
+ " And so on until round 100.\n",
+ " At each visit, the hat status of the cat is toggled (if no hat, put one on; if has hat, take it off).\n",
+ "\n",
+ " Args:\n",
+ " None: The function works with a fixed setup of 100 cats and 100 rounds\n",
+ "\n",
+ " Returns:\n",
+ " - An integer representing the number of cats wearing hats after all 100 rounds are complete\n",
" \"\"\"\n",
- " Write your solution here\n",
- " \"\"\"\n",
- " pass"
+ " return"
]
},
{
"cell_type": "markdown",
- "id": "79",
+ "id": "100",
"metadata": {
- "jp-MarkdownHeadingCollapsed": true,
- "tags": []
+ "jp-MarkdownHeadingCollapsed": true
},
"source": [
- "## Toboggan trajectory 🌶️🌶️🌶️"
+ "## Exercise 4: Base converter\n",
+ "\n",
+ "**Difficulty:** 🌶️🌶️🌶️"
]
},
{
"cell_type": "markdown",
- "id": "80",
+ "id": "101",
"metadata": {},
"source": [
- "During a winter holidays break, your friends propose to hold a [toboggan](https://en.wikipedia.org/wiki/Toboggan) race. While inspecting the map of the place where you decided to hold the race, you realize that it could be rather dangerous as there are many trees along the slope.\n",
+ "Write a function that converts numbers between bases 2-16.\n",
+ "Your function must:\n",
"\n",
- "The following is an example of a map:\n",
+ "##### (1) Process the input number digit by digit (don't use `int()` on the whole number)\n",
+ " \n",
+ "##### (2) Handle these specific requirements\n",
+ "- Accept negative numbers (starting with `-`)\n",
+ "- Skip spaces in the input (e.g., `1010 1111` is valid)\n",
+ "- Accept letters `A` through `F` (or `a` through `f`) for non-decimal bases\n",
"\n",
- "```\n",
- "..##.......\n",
- "#...#...#..\n",
- ".#....#..#.\n",
- "..#.#...#.#\n",
- ".#...##..#.\n",
- "..#.##.....\n",
- ".#.#.#....#\n",
- ".#........#\n",
- "#.##...#...\n",
- "#...##....#\n",
- ".#..#...#.#\n",
- "```\n",
+ "##### (3) Implement proper validation\n",
+ "Your function should raise a `ValueError` exception if any of the following rule is **not** respected:\n",
"\n",
- "A `#` character indicates the position of a tree. These aren't the only trees though, because the map extends **on the right** many times. Your toboggan is an old model which can only follow certain paths with fixed steps **down** and **right**.\n",
- "\n",
- "You start at the top-left and check the position that is **right 3 and down 1**. Then, check the position that is right 3 and down 1 from there, and so on until you go past the bottom of the map."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "81",
- "metadata": {},
- "source": [
- "#### Part 1 🌶️"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "82",
- "metadata": {},
- "source": [
- "
\n",
- "
Question
\n",
- " How many trees would you encounter during your slope?\n",
- ""
- ]
- },
- {
- "cell_type": "markdown",
- "id": "83",
- "metadata": {},
- "source": [
- "
\n",
- "
Hint
\n",
- " Read the trees map as a nested list where each #
corresponds to 1 and each empty site is 0\n",
- ""
- ]
- },
- {
- "cell_type": "markdown",
- "id": "84",
- "metadata": {},
- "source": [
- "
\n",
- "
Hint
\n",
- " In the
solution_
function, define
4 variables:\n",
- "
\n",
- " - the position (starting at
[0, 0]
) \n",
- " - the number of trees encountered
\n",
- " - the depth of the map
\n",
- " - the width of the map
\n",
- "
\n",
- "
\n"
+ "- Bases must be between 2 and 16 (inclusive)\n",
+ "- Each digit must be valid for the source base\n",
+ "- Input number string must not be empty"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "85",
+ "id": "102",
"metadata": {},
"outputs": [],
"source": [
"%%ipytest\n",
"\n",
- "trees_map_str = pathlib.Path(\"tutorial/tests/data/trees_1.txt\").read_text() # do NOT change this line\n",
+ "def solution_base_converter(number: str, from_base: int, to_base: int) -> str:\n",
+ " \"\"\"Converts a number from one base to another.\n",
"\n",
- "trees_map = []\n",
+ " Args:\n",
+ " number: String representation of the number to convert\n",
+ " from_base: Base of the input number (2-16)\n",
+ " to_base: Base to convert to (2-16)\n",
"\n",
- "for line in trees_map_str.splitlines():\n",
- " row = []\n",
- " for position in line:\n",
- " # TODO\n",
- " # For each position, add 1 to `row` if you meet a '#', 0 otherwise\n",
- " # TODO\n",
- " # add `row` to the `trees_map` list\n",
+ " Returns:\n",
+ " - String representation of the number in the target base\n",
"\n",
- "\n",
- "def solution_toboggan_p1(trees_map, right=3, down=1):\n",
+ " Raises:\n",
+ " ValueError: If bases are invalid or if input contains invalid digits\n",
" \"\"\"\n",
- " Complete your solution with the given hints\n",
- " \"\"\" \n",
- " pos = [0, 0]\n",
- " trees = # TODO\n",
- " depth = len(trees_map)\n",
- " width = # TODO\n",
- "\n",
- " # Hints:\n",
- " # - write a loop until you reach the bottom of the map\n",
- " # - if the current location is a tree, add 1 to `trees`\n",
- " # - update `pos` by moving 3 right and 1 down\n",
- " \n",
- " return trees"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "86",
- "metadata": {
- "tags": []
- },
- "source": [
- "#### Part 2 🌶️🌶️"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "87",
- "metadata": {},
- "source": [
- "You check other possible slopes to see if you chose the safest one. These are all the possible slopes according to your map:\n",
- "\n",
- "- Right 1, down 1\n",
- "- Right 3, down 1 (**just checked**)\n",
- "- Right 5, down 1\n",
- "- Right 7, down 1\n",
- "- Right 1, down 2\n",
- "\n",
- "
\n",
- "
Question
\n",
- " What do you get if you multiply together the number of trees encountered on each of the above slopes?\n",
- ""
- ]
- },
- {
- "cell_type": "markdown",
- "id": "88",
- "metadata": {},
- "source": [
- "
\n",
- "
Hint
\n",
- " Define a variable slopes
as a list (or tuple) containing lists (or tuples) of the slopes' steps above\n",
- ""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "89",
- "metadata": {},
- "outputs": [],
- "source": [
- "%%ipytest\n",
- "\n",
- "slopes = ((3, 1), ) # TODO\n",
- "\n",
- "def solution_toboggan_p2(trees_map, slopes):\n",
- " total = 1\n",
- " for right, down in slopes:\n",
- " # TODO\n",
- " # use your solution of Part 1 to calculate the trees for a given slope\n",
- " # accumulate the product in `total`\n",
- " \n",
- " return total"
+ " return"
]
}
],
@@ -1400,7 +1933,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.10.13"
+ "version": "3.10.15"
},
"toc-autonumbering": false,
"toc-showmarkdowntxt": false,
diff --git a/tutorial/tests/data/trees_1.txt b/tutorial/tests/data/trees_1.txt
deleted file mode 100644
index 8985593f..00000000
--- a/tutorial/tests/data/trees_1.txt
+++ /dev/null
@@ -1,323 +0,0 @@
-..#..#......###.#...#......#..#
-...#.....#...#...#..........#..
-....#.#...............#.#.#....
-.........#.......##............
-#.#....#.#####.##.#........#..#
-.....#...##.#..#.##...#.#..#...
-#.#..###.#........#....##...#.#
-..###.....#..###.....##........
-#.#.#...........#.....#.#....##
-...#.#.##.##.#.#......#...##.#.
-.....##.#..#....#..#...##...###
-...#.....#..#..#...#.#....##...
-.#...##.#.........#...#.#......
-....#...#.....#......#...#.....
-.#...#.....#....#......#...#...
-#...#......####..##...###......
-....#..#......##.##.....#..#...
-....#....#.......#..#...#....#.
-...##..#.##..#.#...#..##.......
-##.#..#.....#.##.#....#..##....
-#....#....#.....#..#.#.#.....#.
-##...#.###.....#....#..#.#.#...
-#..#.......#...#.#...#.#.....#.
-....#.#.......#.....###..#..#.#
-......####...#.#..#..#.#.#.#...
-#...##.....#...#.#.........#.#.
-......#...##.#..#.#........#...
-..#.#...........#..##...###.##.
-#......#.#......#.....#.....#.#
-.#...............###.#.###.....
-...#...........##..#...##..##.#
-#......#.##.#............#.##.#
-.#.#....#....###........#..#...
-...##.#.#..#.##.#..##..#.##..##
-.....#...#.#.#...#....#......#.
-..............#...##...........
-..............##........#..###.
-.#.##.......#.....##.#......#..
-..#......#..#.#####..#.#.......
-#.#..#...#.#..#....#..#.##..#..
-...##.......#.#............#...
-...#....#..#.##.###.......#.###
-..###..#....#..#.....##...#..#.
-..#.###.##......###....#....##.
-...#...##...###....##.....###.#
-.....#.....#.#.#.........#..###
-#.#......#.#..#.####..#........
-#....#.##.......##.............
-..##...........#....#.....##..#
-..#...#...........#....#...#...
-...#...#...#.....#..#....#....#
-#......##.........#.#...##...#.
-.##..#...#.....#....#.##.####.#
-#..##.##.#......#.............#
-.#.....#..##.###.#.#.#.........
-.###....###..#....#..#.#.#..##.
-....#........#..#....##..#.#.#.
-.....#..........#..........#...
-.#.##..#..#...#..#.##.#.##.....
-.#....#...#......#.#..##.##..#.
-.###.#...#.#.##....#.....#..##.
-......##.......#..#.......#.#.#
-.##.#.#.#......#.......#.......
-#..#...##......#.......#......#
-...#..#...##.#...#..##.........
-.....#..###...##...#..#.#...#.#
-..#.#.#....##..#.#.#.#...#.....
-.....#.#.#..#..#.#.#...#.......
-#.#.#...#.#.....#.#.#.##.###...
-.....#.#.....####..#...........
-..#.#.#...........##..#.#....#.
-.#..#......#..#...........###..
-..#...###.##......#..###...#..#
-#.#..#.....#..#.##.#..#.#.....#
-.....................#.#..#....
-...##..##...#.#..#..##.#....#..
-.#..#.#....#...#.#.##..........
-....##.....#..#..##.........##.
-..##...##........#.#....#...###
-.#...#............#.#.#.#......
-#...#........#..#..#...#.#.....
-..#..........#.......###.##....
-#...........###..#....##..#.##.
-##...#..#.##.....#...........#.
-.#..##.....#..#.#.....##.#..#.#
-..#..#.##....#.........#.#.#...
-#..#...#...#..#...........##...
-.....#.......#.#......#.#.#...#
-..#.#..#..#.#.#.......#.#...#..
-......#.....##.....#.....##.##.
-#.#..#......#......#.####.##...
-.####...#####.#....#.#..##.....
-............#....#....#....##..
-###.........#............#.#...
-...#...#....#.##..#...#......##
-...##.#.#.##.##.#.....#...#.#..
-...#.....#...#..##......#.#.##.
-.##.#......##................##
-......#.....#..##.............#
-#.#...##..#..#..#.##.....#..#..
-#......###.....#....##...##...#
-....#..#.....#.......####...##.
-#.#...#.#...#..........#..##..#
-....#..#....#................##
-.####..#........#..#.#...#.....
-##.###...#.##........#..##.....
-..###..##...#...#..#...##.....#
-......#..##....................
-.#...#......#.#.##..#........#.
-..#...#####.....##.....#...#...
-.#..#....#..#....##.#....#..##.
-.#.....##..###.#.....#.#.#.##..
-#..##.....##...#.....#..#.#....
-#.##......#.#......#..........#
-#####........#.............#...
-.#..#..##..#....#.....#..####..
-...#..##.##...####....#.##...##
-..........#....#...........##.#
-#...##...#...##....#.....#.....
-.......#..#.....#.#.#.#.#.....#
-...#..##..####..#..##.#.##....#
-#...#...#...........#.#.....#.#
-..#.....##...###.........#..##.
-.......##..#.......#.......##..
-#.#....#....#.###............#.
-...#......#.#.............#.#..
-......#..#....#....#....#..#...
-.....##..#...........##...#.##.
-..#....#.##.#......#...........
-#...#....#.#.#.#.#..#..........
-.#..#..........#..#.#.....#....
-.....##......##....#.#.....#.#.
-.....#..#..........#....#.....#
-....#..#..#.#...#.#..#..#..##.#
-.#..##.#..##...###.#..........#
-..###..#......#...##...#.#.....
-..#...#...#.....#.......#....#.
-#...##..#.##.#....##.....#.....
-..#.#.....#...#...#............
-.......#.#.#..#.....###.#...##.
-....##.......#####...##..##..#.
-#...#.##.....#.#...##.........#
-..#.##..........#..###.#....#..
-#......#.##...#...#.....###....
-................#.##...........
-##.###.#.#.#.##......##..#....#
-..#.#........##..#..##.........
-###....#..#....#..##....#.....#
-#......#..#...........#.#...##.
-...###.......#...#......##..#.#
-.......#...##.#.#...#.##......#
-......##..#...##.#.#...##....#.
-..#...#...#...#.#.....#..##..#.
-..##...#.....#.....#..##.......
-....#........#.#.##.......#.#..
-#...#..##..#..##..#...#......#.
-...#..#.#.#..#..#..####...#....
-#..#..#......#......#..#.######
-#..#..#..#........#..#.#....###
-#..##..#.#.##.....#..#......#.#
-##.......##.#..#.............#.
-..........#.#..#..#............
-....#.#.#.#...#......#......#..
-###.#.#.........#.......#...##.
-#.............####..#...#.##...
-....##.......#................#
-###...#..#......##....#.####.#.
-..##.##.#.#.#.#...#.......#...#
-.....#.##......#.......##..#.#.
-.#...#.##..#.......#.#....#.#.#
-##...##..#....#..#...#....#....
-..........#...##.#..##.......##
-#.#...#....#......#.#.......###
-......#...#.##....#....##.#.##.
-..#..#.......#.......#....##...
-##..##.......##............#.#.
-.#.#...#..#.#.###......#.......
-#...#..##....#...###..#.#.....#
-.#.....#........#..##.#.#.#....
-..#.##....#..#...........#...#.
-.....#.#...#.##..###...#...#...
-#....####.......#..#.#...#.....
-....#.....#....##..#.##.....###
-........#.#.....###....#.#.....
-...#.....#.##.....#......#.....
-.....#...####......###..#...##.
-#.#......#..........#..##.#..#.
-..##......###...#...#.......#..
-#...#.#...#.#.........#........
-....#..#.##.#.##.###..#.....#..
-.#.#.#......#.#........#.....#.
-.....#.#..#....#...#.....#.#.##
-##.............#..#.....#.#....
-#............#..#....##......##
-#....#......#......#....##..#..
-.#....#............#......##..#
-..#.#.#..#.#....##.#.......#.##
-#.##.....#...#......#...#......
-.......#...........#..#.##..#.#
-##.....##.#.....####..........#
-...#.......#.#.............#..#
-...##........##..#..#.#........
-.#.##...#.....##.#......#....#.
-.#................#.#...#..#...
-#....#.#.#......#.#.#.##....#..
-..#......#............#...#....
-###..#.##........#....##.#...#.
-.#..#..#......##...............
-....##.............#....##...##
-..#.#..#.#####....##.......###.
-......#...#..#.#....#.#..#...#.
-.........#..##.##...#....##..##
-.............#.##....###.#.....
-..#................#..#.#..#...
-...#........#......#..###......
-.#.#.#....#.........#...###.###
-.........#..#.#......##.....#..
-#...##..#.#.###..###...........
-...#.#.#..#......#..##.#.##....
-.....##.......#................
-.##....#.#.#.##.....#.##......#
-...#........#...##.#.##..##...#
-..#..........#.#......####..##.
-............#.#.#.#.....#......
-..##.####.#..#....#..#..##.....
-......#........#...#..#.#..###.
-#.#..............#..#...#..#...
-....#............#...#..#...##.
-..##....#...##.##.#..........##
-..#..#.........#..#.....#.#....
-#.....#.###...##...##...##.....
-#.#...#..#####.#...#..#.....#..
-..#.....###...#.........#.#...#
-....#.##.........#.#.....#.#.#.
-..........##...#....#.#.#.....#
-...#...........#.....###.......
-#....#..#...#.....#.......#....
-.#.#.....#..##..##..#........#.
-.#.#.....#....#...#.#.##.......
-....###...#...###.##....#......
-...#.#.##....#...##......#...#.
-#....#...##.....#.##.#.....#.##
-.#.#.....##.##.##..###...#.....
-.#.#......#..#..#........#.#..#
-........#...##........##...#...
-.#..#.#.#..#.....#....#...#.#..
-#......#...#.#...#..#.#..#.....
-.#......#.....#.........###.#..
-#..#..........##..###.......#..
-#..#..#....#......#......#.....
-......#.....##.........##....#.
-#..#.#...#...#.##.#..#..##.....
-....#.#....###..#.....#...##.#.
-..##.....##.#..#..##..#.#......
-.........#..#....###...#.#....#
-.........#...#...#...#......##.
-.......#..#.....#.#.#...#...#..
-............#.....###......#..#
-#....##..###.......#...##....##
-..#.##..#####..##.#...#......#.
-#.#..#...###.............#.#...
-##...#..#..#.#....#.#.......#..
-.....#....##.....###.##..#.....
-......##..##..#.#..####.#......
-..#...#.#....#...#.#.........#.
-##.....#.#....#..#..##........#
-...........#..#........##..#...
-..##.#...#.#.#..##..#..#..#..##
-..........#.###.....#..#.....#.
-......#............###..##.##..
-.#.......#..#...........#.###.#
-#...#..##............##.......#
-.###..#...#.#....#....#......#.
-..##.........##............#.#.
-.##.......##....#.#.#....#..#.#
-#.##........#.....#.##...#.#...
-#......#....#.#......##....#..#
-#.##..##..#...#.###......#.....
-..........#.#....###.#.....##..
-#..##...#.###..#.............#.
-.#.#......#.##.#...#....#.....#
-.##...#..##...#...........#.##.
-.##..#.#.#..#.....#.....###....
-.#...#.#.#..#..#....##...#..#..
-#.#.#....#.....#..#..##..#.#...
-......#..#...####..#.........#.
-.#.#..#......#...#..####.....#.
-...#.#...#...#....##..#.#.#.##.
-...#........##.............#.#.
-...#...#...#.......#..#.#.#..##
-.####.#...##......#.##.##.#.#..
-#..###...........#..#.#...#.#.#
-###...#.#..#...#.#...#.#..#.#.#
-#....#.....##...#.#...#..#.#...
-.#........##.##....##..#..#....
-.#.#.#..#........#...#..#.#.#.#
-#.##.....#.#...#....##...#..#.#
-..#.......##.#.###............#
-##....###..##.........##..#.#..
-...##...#...#..###.#.....##..#.
-###.................#.#..#.....
-....#......#.....#..###......##
-.......#...##..#...............
-.#.....#..#.....#...##...#...##
-.....##....#.#..#.##.....#...#.
-#..####.#....#..#.....#....#..#
-..#..##.#.##......#..#.#....#..
-..#.#.#.#.....#...#...#..#.....
-.#........#.#...#.#..#...##....
-.#...#.#...#..#.#...###...#.#..
-#.....#...##..#.....#...#.#..#.
-...#....#................#.#...
-......##.#.#..........#...#....
-.##..#.#.#...#..#...####.#.....
-#......#....#..#.......#.......
-.#........#.#.#....###.#..##...
-....##......#.....##...#...#...
-..#..#.#.#...#..#.####.##......
-...#........#.#.##.#..#.##.#...
-.#..##...#...#...##.......##.#.
-#...#.#......#.................
-..#..#.....#....##...#..###....
-.#...#.........#.#.##.#........
diff --git a/tutorial/tests/data/trees_2.txt b/tutorial/tests/data/trees_2.txt
deleted file mode 100644
index e1bd0f65..00000000
--- a/tutorial/tests/data/trees_2.txt
+++ /dev/null
@@ -1,323 +0,0 @@
-.#.......#...........#.........
-..##.......#.#.#.....##...#....
-.......#..#.....#...#..........
-...#..........###...#........##
-#.#..#.#.##.#........#.#.....#.
-#..#....#..#....#..............
-#..#........#..................
-..#.#...#.#...#....#.#.#..#....
-..............#..#.............
-.##....#...................#...
-........#..........#......#...#
-.##..#..#...##..........#...#..
-.#...#....#.........#...#.....#
-.#........##............#.#....
-...........#..............##...
-.#..#......#..#..............#.
-..#.#.#...........#........#...
-..###..........#....#.#......#.
-.......#...##..........#.......
-........#...#..................
-....#....#..#.......#........#.
-.......##.#......#.....#...##..
-..#.#........................#.
-.#.....#.##..............#.#...
-..#.#...#.#..#....#....#.......
-.#....##.....#....#........#...
-..#...........#.##....#...#....
-..#.##...#....#.#.....##...#...
-.......#...####...#...#.......#
-.#...#.........................
-.......................#.......
-.....#.#.........#..........#.#
-#.........#............###..#..
-.....#.#.............###.......
-...#..#........#.#.......#.....
-...................#....#......
-...#..#...#............#..##...
-...#.....#....#.......##......#
-.....#....#...##..#..#...#...#.
-..........#...........#.#.#....
-..#.......#...#.....#......#...
-.........#.......##......#..#.#
-..#.....#..#.###...#.#......#..
-#....#...#..#...#.....#........
-..#......#..#.......#.#.....#..
-#......#...#......#.....##.#...
-........##.......#.......#.....
-.#.#...............#...........
-..............#...#.#....#.....
-....#......#.#..#......#.......
-...##....#....#...#............
-.#...............#...........#.
-.#.#...#.#.....#.....#...#.#...
-...##...........#.....#..#...#.
-.#.#...##.#.#......#......#....
-.##.....#.......##....#.#.#....
-.......#...........#....#....#.
-....#...........#......#.####..
-......#....#...#...##.......#..
-......................#.#####..
-..#...#.#...#..#..#......#.....
-....#........##.......##....#..
-#.#......##.........##.#..#...#
-.#.#....#...#..#...#...##....#.
-.....##...#....#....#.#........
-......#..#....#.#...#..........
-.........#...................#.
-............#.###....#.#.......
-...#.#.....#......#....#.#..#..
-..............#..#.#.#.#.......
-#..##...................##.....
-..#.......#..#.........##..#...
-.........##...#......#........#
-..#.........#........##.###.#..
-...........#.#....#.....###....
-..#....##.#..#.##....#.....##..
-..#.....#.##..................#
-#....#.........................
-..............#..#...#.#.......
-......#..#.#.##....#..........#
-..#.........#.####.....#.......
-......#..#.#..........#...#....
-......#.................#..#.#.
-.....#..........#..............
-....#.....#............#....##.
-.....#.....#........#..........
-............#.....#...#........
-........#....#.#...............
-#.....#.........#......#..#.#..
-...#..#......#......#.......#..
-.....#......#.#....#..#...#...#
-......................#..##....
-.............#.........###....#
-#..............#.#..........##.
-...#.#.................##......
-...........#.#.....#...........
-.........#.................#.#.
-........#........#...#..##...#.
-........#......##.......###....
-..............#.#.#............
-.#.....###...##.#......#.....#.
-.............#......#.#.#...#.#
-..#.........#.......#.....#....
-......#........#...##......#...
-.##..........##......#.#.....#.
-..#.##....#....#...............
-......#...#..#.....#.....#...#.
-.......##..##..#............##.
-..............#...##........#..
-#....#................#..#.....
-........#.......#.#.#...#......
-......#.......#..............#.
-#.#..#...#........#....#..####.
-..#........#...........#.....#.
-.##...........................#
-.............#...........#.....
-.#.....#.#...#.........#.......
-..........#...#....#....#......
-.#..#........##....#...........
-.......###......##...#.........
-..........#.#.#..#.#....#......
-........##..#.........#....#...
-........#.#......#.#...#.#..#..
-....#....................#.##..
-##....#..#...........#.....#.#.
-...#..............##...##..#.#.
-......#.##.#.......#..#...#....
-....#..#..##.....#.....#.#....#
-.......#....##.##..............
-#..##....#.....#.#.............
-..................#......#..#..
-..#......#...#..#.......#...#..
-...........#....#.#.....#......
-#..#...##.........###..#......#
-.......#......................#
-#.......#....................#.
-..#..#..........#..#..#....#...
-.##..#..#.....#.#..##..........
-#..###.......#..##..#...#..#.#.
-.....##......###.....#.#.##...#
-..............#...#....#.#.....
-#...........#..................
-..............#....#..##..#..#.
-.........#.............#.......
-.#.#....#....#...............##
-.##.##.#.....###.....#.........
-....#..............##......#...
-....#........##................
-....#.....#....#....##....##...
-.#........#......#......#......
-....#..........#...............
-##..........#......#.....#.....
-........#.#..#.#..#.....##.....
-..##......#.#.......#.#..#.....
-.#.......#......#...........#..
-..#.#..#.#..................#..
-...#...#...#...##......#.......
-.#...##....#...#...#...#.......
-.......#.#.......#.............
-.#.##.#.....#...........#.##.#.
-.#.##.#........#...##..........
-.#.....#.....#....#..#.........
-...##.............##...........
-.#........##.....#.......#...#.
-...........#..#..##........##..
-.....#..#......................
-..#.......#....................
-.....#......#....#....#.......#
-........#..#.#.....#......#....
-..........#..#.....#......#....
-..........#####.....#........#.
-........#..#...#.#....#......#.
-.........#...#....#.#..........
-......#....##..........#...#...
-#..............###.#.#.........
-.#.#............##......#.#..#.
-......#........................
-...#..#......#.......#....#...#
-.......#....##.....#.#......##.
-...........#..........#..#.....
-...........#..#.....###......#.
-.......#....#..##......#.......
-.........#.#.#.......#..#...#..
-.......#.......##.....##...#...
-..............#....#.....#.....
-...#....#.....#.#..........##..
-###.........#.............#....
-...##......#.#........#....#..#
-#....###.......#...#.#......##.
-....#...##.......#......#.....#
-.....#......#..................
-#........##....#....#.#........
-........#.......###...#........
-........#..#.......###.........
-..............#......#..#......
-#......#.....#....#.#..........
-.#......##.#.#.....#...#.#....#
-.##...........#..#.##.....#....
-.....#.....................#...
-.#..#...#...##.#...#...........
-.......#.......##..#.#..#......
-.......##.....#.....#..........
-.................#.............
-#........#..#.......##.........
-#...#..###.#..#....#.#.###.....
-..#.......#.......#.......#....
-..............#............##..
-.#...#..#...##.........#....#..
-#...........#...#..............
-.......#.....#......#..#.....#.
-..........#......#.............
-##.........###..##.#....#..#.#.
-..............###..............
-#..##.............##.....#.....
-....##...................#..#..
-....#.....#..............#..#.#
-........#........##...#.....##.
-#...........#.##..........##...
-#......##.....#...............#
-..##..#....#.................#.
-#.......##.....................
-...............#.##..##......#.
-..#.##..#.#....#.......##......
-......##....#............##....
-.#..#..##.....#.##....#........
-#.........#..........#...#....#
-...#.......#.............#.#.#.
-..##............#...##........#
-.......#.#.#........#..........
-.....#.............#.....#.....
-.........#.........#.........#.
-#.....#....#.......#...........
-.........#....#.............#.#
-.##..#.......#...#......#......
-....#....#....#........#....#..
-............#.......#..#......#
-.#............#.##........##...
-..#...##...#....#...#.#...#..#.
-#...#..........#..##.........#.
-..#.........................#.#
-...........#.........#..#.##...
-.#..................#..#.......
-......#......#...........#..#..
-...##.....#.....#..#.......#...
-.........#.#.......#......#....
-...........#................#..
-.....#...#..#............##....
-.#.......#..#....#..........#..
-#.....#..#.....#..##.......##..
-...#.......#...#....#...#.#..##
-...#...##......#....#....#.....
-.......###.#..#.......#......#.
-........#.#...#..#..#...#....#.
-....#.........##.#.....#.......
-....#.........#..##........#...
-..#...........#......#....#.##.
-.....................#.........
-...................##......#..#
-......#.#.....##..##..........#
-..#.##........#.#.#..........#.
-.#.......#...##.#....#....#....
-#.#......#..#..#.......#.......
-.............#........#.......#
-....#...#.....#........#...#...
-..#..............##..#.........
-..#.................#..#...##..
-....#..#...#...................
-......#.........##.#..#..#...##
-........#..#....#.......#.#.##.
-.#...#...........#..........#..
-##.....#...#............##...#.
-.##.....#...#..................
-.#.......####.#..##.##.#......#
-.............#...#..#..#.......
-...#.##.........#.#....#.......
-...........##...##....#....##..
-........#......#...#...........
-...........#..#...#....#.##....
-..##....#..........#....#...#..
-#....#.#.#.......#.#...........
-......#............##..........
-#.#.###..#....#.......#...#....
-.#......##..#..#.#.........#..#
-..#.........#........#....#....
-......##.#.......##....#..#..##
-.............#...#............#
-......#......#...#.#.#.##.#....
-#.#...#.##.....#..............#
-..........#.............##.##..
-#......#....#...#.#.#.#..#....#
-........#........#...#.#......#
-.....#...........#.............
-...........#....#..........#...
-....####...#..##....#.#........
-.#......#...#..#...........#...
-#......###..#.##.###...........
-..#...........#.........#....#.
-................#.#....#..#.##.
-...................#......#....
-....#.#.....#.......#...###.##.
-.#........#.#....#...#..#...#..
-....#..###.................#..#
-.....#.#..#........#......#..#.
-....#.....#...............#...#
-............##.#.........#..#..
-.......#..#..##.#.#...##.......
-..#..........##..#..#........#.
-..............#..#...#.........
-......#.#....#........##.......
-.#.....##....#..#...#.......##.
-..............#.##.............
-#..#..#...##....##.#.....##.#..
-..#...###..#.........##........
-........##......#.....#..###...
-.....#......##.###.............
-....#.....#.#..#.#..#..........
-....#..#.......#...........#...
-.#.............#..#......##....
-..#.#......#.#.................
-.......#.#.#............#..#...
-......###....##............#..#
-.........#....#......#.........
-..........#...............#..#.
diff --git a/tutorial/tests/test_control_flow.py b/tutorial/tests/test_control_flow.py
index 56cfd2b8..8e8c434d 100644
--- a/tutorial/tests/test_control_flow.py
+++ b/tutorial/tests/test_control_flow.py
@@ -1,18 +1,14 @@
+import contextlib
import pathlib
-import sys
-from collections import Counter
from math import isclose, sqrt
-from typing import List, Tuple
+from typing import Any, List, Optional, Tuple
import pytest
def read_data(name: str, data_dir: str = "data") -> pathlib.Path:
"""Read input data"""
- current_module = sys.modules[__name__]
- return (
- pathlib.Path(current_module.__file__).parent / f"{data_dir}/{name}"
- ).resolve()
+ return (pathlib.Path(__file__).parent / f"{data_dir}/{name}").resolve()
#
@@ -20,9 +16,12 @@ def read_data(name: str, data_dir: str = "data") -> pathlib.Path:
#
-def reference_indexed_string(string: str) -> list[tuple[str, int]]:
+def reference_indexed_string(string: str) -> List[Tuple[str, int]]:
"""Reference solution warm-up 1"""
- return [(char, index) for index, char in enumerate(string)]
+ result = []
+ for i, char in enumerate(string):
+ result.append((char, i))
+ return result
@pytest.mark.parametrize(
@@ -46,7 +45,7 @@ def test_indexed_string(string: str, function_to_test) -> None:
assert reference_indexed_string(string) == result
-def reference_range_of_nums(start: int, end: int) -> list[int]:
+def reference_range_of_nums(start: int, end: int) -> List[int]:
"""Reference solution warm-up 2"""
step = 1 if start < end else -1
return list(range(start, end + step, step))
@@ -68,9 +67,13 @@ def test_range_of_nums(start: int, end: int, function_to_test) -> None:
), "The function returned an empty range"
-def reference_sqrt_of_nums(nums: list[int]) -> list[float]:
+def reference_sqrt_of_nums(numbers: List[int]) -> List[float]:
"""Reference solution warm-up 3"""
- return [sqrt(num) for num in nums if num >= 0]
+ result = []
+ for num in numbers:
+ if num >= 0:
+ result.append(sqrt(num))
+ return result
@pytest.mark.parametrize(
@@ -106,11 +109,11 @@ def test_sqrt_of_nums(nums: list[int], function_to_test) -> None:
), "The function should return the square root of each number"
-def reference_divide_until(num: int) -> int:
+def reference_divide_until(number: int) -> int:
"""Reference solution warm-up 4"""
- while num % 2 == 0:
- num //= 2
- return num
+ while number % 2 == 0:
+ number //= 2
+ return number
@pytest.mark.parametrize(
@@ -120,6 +123,111 @@ def test_divide_until(num: int, function_to_test) -> None:
assert reference_divide_until(num) == function_to_test(num)
+#
+# Exercise: conditionals inside loops
+#
+
+
+def reference_filter_by_position(numbers: List[int]) -> List[int]:
+ result = set()
+ for pos, number in enumerate(numbers, start=1):
+ if number > pos:
+ result.add(number)
+ return sorted(result)
+
+
+@pytest.mark.parametrize(
+ "numbers",
+ [
+ [0, 3, 1, 2], # Basic case from example = {3}
+ [5, 4, 3, 2, 1], # Decreasing numbers = {4, 5}
+ [1, 3, 5, 7, 9], # All odd numbers = {3, 5, 7, 9}
+ [], # Empty list = {}
+ [0, 0, 0], # Same numbers with none valid = {}
+ [2, 2, 2, 2], # Same number with one valid = {2}
+ [4, 4, 4, 4], # Same number, three are valid but they are duplicates = {4}
+ [10, 20, 1, 2, 3], # Mixed large and small numbers = {10, 20}
+ ],
+)
+def test_filter_by_position(numbers: List[int], function_to_test) -> None:
+ """Test filtering numbers by position."""
+ assert function_to_test(numbers) == reference_filter_by_position(numbers)
+
+
+#
+# Exercise: breaking out of loops
+#
+
+
+def reference_find_even_multiple_three(numbers: List[int]) -> Optional[int]:
+ result = None
+ for number in numbers:
+ if number % 2 == 0 and number % 3 == 0:
+ result = number
+ break
+ return result
+
+
+@pytest.mark.parametrize(
+ "numbers",
+ [
+ [1, 2, 3, 4, 6, 8], # 6 is first even multiple of 3 = 6
+ [1, 3, 5, 7, 9], # No even numbers = None
+ [12, 18, 24], # All are valid, should return the first = 12
+ [], # Empty list = None
+ [2, 4, 6, 8, 10], # Even numbers but no multiples of 3 = None
+ [1, 3, 5, 7, 12], # Valid number at the end = 12
+ ],
+)
+def test_find_even_multiple_three(numbers: List[int], function_to_test) -> None:
+ """Test finding first even multiple of 3."""
+ assert function_to_test(numbers) == reference_find_even_multiple_three(numbers)
+
+
+#
+# Exercise: using else in loops
+#
+
+
+def reference_is_pure_number(text: str) -> bool:
+ for char in text:
+ if char not in "1234567890":
+ return False
+ else:
+ return True
+
+
+def is_for_else_used(function) -> bool:
+ import ast
+ import inspect
+
+ tree = ast.parse(inspect.getsource(function))
+ for node in ast.walk(tree):
+ if isinstance(node, ast.For) and node.orelse:
+ return True
+ return False
+
+
+@pytest.mark.parametrize(
+ "text",
+ [
+ "123456", # OK
+ "0987654321", # OK
+ "", # Empty
+ "abc123", # Mixed characters
+ "0000", # All zeros
+ "12.34", # With decimal point
+ " ", # All spaces
+ "-123", # With negative sign
+ "١٢٣", # Non-ASCII digits (should return False)
+ ],
+)
+def test_is_pure_number(text: str, function_to_test) -> None:
+ """Test checking for pure number strings."""
+ assert is_for_else_used(function_to_test), "You must use a for-else construct"
+ assert function_to_test(text) == reference_is_pure_number(text)
+
+
#
# Exercise 1: Find the factors
#
@@ -127,7 +235,11 @@ def test_divide_until(num: int, function_to_test) -> None:
def reference_find_factors(num: int) -> List[int]:
"""Reference solution to find the factors of an integer"""
- return [m for m in range(1, num + 1) if num % m == 0]
+ factors = []
+ for m in range(1, num + 1):
+ if num % m == 0:
+ factors.append(m)
+ return factors
@pytest.mark.parametrize("num", [350, 487, 965, 816, 598, 443, 13, 17, 211])
@@ -145,7 +257,30 @@ def test_find_factors(num: int, function_to_test) -> None:
)
-def reference_find_pair(nums: List[int]) -> int:
+def reference_find_pair(nums: List[int]):
+ """
+ Reference solutions:
+ - A solution with two nested loops
+ - A solution using a dictionary and a single loop
+ """
+
+ def find_pair_with_double_loop(nums: List[int]) -> Optional[int]:
+ """Two nested loops"""
+ for i in nums:
+ for j in nums:
+ if i + j == 2020:
+ return i * j
+
+ def find_pair_with_sets(nums: List[int]) -> Optional[int]:
+ """Using a dictionary and a single loop"""
+ complements = {}
+ for num in nums:
+ if num in complements:
+ return num * complements[num]
+ complements[2020 - num] = num
+
+
+def __reference_find_pair(nums: List[int]) -> Optional[int]:
"""Reference solution (part 1)"""
complements = {}
for num in nums:
@@ -156,20 +291,39 @@ def reference_find_pair(nums: List[int]) -> int:
@pytest.mark.parametrize("nums", [nums_1, nums_2])
def test_find_pair(nums: List[int], function_to_test) -> None:
- assert function_to_test(nums) == reference_find_pair(nums)
-
-
-def reference_find_triplet_slow(nums: List[int]) -> int:
- """Reference solution (part 2), O(n^3)"""
- n = len(nums)
- for i in range(n - 2):
- for j in range(i + 1, n - 1):
- for k in range(j + 1, n):
- if nums[i] + nums[j] + nums[k] == 2020:
- return nums[i] * nums_2[j] * nums[k]
-
-
-def reference_find_triplet(nums: List[int]) -> int:
+ assert function_to_test(nums) == __reference_find_pair(nums)
+
+
+def reference_find_triplet(nums: List[int]):
+ """
+ Reference solutions:
+ - A slow solution with three nested loops
+ - A fast solution using only two loops
+ """
+
+ def find_triplet_slow(nums: List[int]) -> Optional[int]:
+ """Slow solution with a triple loop"""
+ n = len(nums)
+ for i in range(n - 2):
+ for j in range(i + 1, n - 1):
+ for k in range(j + 1, n):
+ if nums[i] + nums[j] + nums[k] == 2020:
+ return nums[i] * nums[j] * nums[k]
+
+ def find_triplet_best(nums: List[int]) -> Optional[int]:
+ """Fast solution with two loops"""
+ n = len(nums)
+ for i in range(n - 1):
+ s = set()
+ target_sum = 2020 - nums[i]
+ for j in range(i + 1, n):
+ last_num = target_sum - nums[j]
+ if last_num in s:
+ return nums[i] * nums[j] * last_num
+ s.add(nums[j])
+
+
+def __reference_find_triplet(nums: List[int]) -> Optional[int]:
"""Reference solution (part 2), O(n^2)"""
n = len(nums)
for i in range(n - 1):
@@ -184,7 +338,7 @@ def reference_find_triplet(nums: List[int]) -> int:
@pytest.mark.parametrize("nums", [nums_1, nums_2])
def test_find_triplet(nums: List[int], function_to_test) -> None:
- assert function_to_test(nums) == reference_find_triplet(nums)
+ assert function_to_test(nums) == __reference_find_triplet(nums)
#
@@ -201,7 +355,7 @@ def reference_cats_with_hats() -> int:
if cat % loop == 0:
cats[cat] = not has_hat
- return Counter(cats.values())[True]
+ return sum(cats.values())
def test_cats_with_hats(function_to_test) -> None:
@@ -209,69 +363,158 @@ def test_cats_with_hats(function_to_test) -> None:
#
-# Exercise 4: Toboggan trajectory
+# Exercise 4: Base converter
#
-def parse_data(filename: str) -> List[List[int]]:
- """Parse a map of trees"""
- input_data = read_data(filename).read_text()
- return [
- [1 if pos == "#" else 0 for pos in line] for line in input_data.splitlines()
- ]
+def reference_base_converter(number: str, from_base: int, to_base: int) -> str:
+ """Reference solution to convert a number from one base to another"""
+ # Validate bases
+ if not (2 <= from_base <= 16 and 2 <= to_base <= 16):
+ err = "Bases must be between 2 and 16"
+ raise ValueError(err)
+ # Handle empty input
+ if not number or number.strip() in ("", "-"):
+ err = "Invalid empty input"
+ raise ValueError(err)
-trees_1, trees_2 = (parse_data(f"trees_{num}.txt") for num in (1, 2))
+ # Same to and from bases
+ if from_base == to_base:
+ return number
+ # Handle negative numbers
+ is_negative = number.strip().startswith("-")
+ number = number.strip().removeprefix("-")
-def reference_toboggan_p1(trees_map: List[List[int]], right: int, down: int) -> int:
- """Reference solution (part 1)"""
- start, trees, depth, width = [0, 0], 0, len(trees_map), len(trees_map[0])
- while start[0] < depth:
- trees += trees_map[start[0]][start[1]]
- start = [start[0] + down, (start[1] + right) % width]
- return trees
+ # Remove spaces and convert to uppercase for consistency
+ number = number.replace(" ", "").upper()
+
+ # Validate digits
+ valid_digits = "0123456789ABCDEF"
+ for digit in number:
+ if digit not in valid_digits[:from_base]:
+ err = f"Invalid digit '{digit}' for base {from_base}"
+ raise ValueError(err)
+
+ # Convert to base 10
+ decimal = 0
+ for digit in number:
+ decimal = decimal * from_base + valid_digits.index(digit)
+
+ # Handle 0 as a special case
+ if decimal == 0:
+ return "0"
+
+ if to_base == 10:
+ return str(decimal)
+
+ # Convert to target base
+ result = ""
+ while decimal > 0:
+ digit = decimal % to_base
+ result += valid_digits[digit]
+ decimal //= to_base
+
+ return f"-{result}" if is_negative else result
+
+
+# We need a way to "disable" the use of `int()`, otherwise it's too easy
+# Solution: replace `int()` with a function that raises an exception using a context manager
+@contextlib.contextmanager
+def block_int():
+ """Context manager to block int() usage"""
+ original_int = int
+
+ class IntReplacement:
+ def __call__(self, *args, **kwargs):
+ import inspect
+
+ frame = inspect.currentframe()
+ while frame:
+ if frame.f_code.co_name == "solution_base_converter":
+ raise AssertionError("Using int() is not allowed.") # noqa: TRY003
+ frame = frame.f_back
+ return original_int(*args, **kwargs)
+
+ def __instancecheck__(self, instance: Any, /) -> bool:
+ return isinstance(instance, original_int)
+
+ import builtins
+
+ builtins.int = IntReplacement()
+
+ try:
+ yield
+ finally:
+ builtins.int = original_int
@pytest.mark.parametrize(
- "trees_map, right, down",
- [
- (trees_1, 3, 1),
- (trees_2, 3, 1),
- ],
+ "number,from_base,to_base", [("42", 10, 2), ("1A", 16, 2), ("1010", 2, 16)]
+)
+def test_base_converter_basics(number, from_base, to_base, function_to_test):
+ with block_int():
+ expected = reference_base_converter(number, from_base, to_base)
+ assert function_to_test(number, from_base, to_base) == expected
+
+
+@pytest.mark.parametrize(
+ "number,from_base,to_base", [("10 10", 2, 10), ("FF FF", 16, 2)]
+)
+def test_base_converter_with_spaces(number, from_base, to_base, function_to_test):
+ with block_int():
+ expected = reference_base_converter(number, from_base, to_base)
+ assert function_to_test(number, from_base, to_base) == expected
+
+
+@pytest.mark.parametrize("number,from_base,to_base", [("-42", 10, 2), ("-FF", 16, 10)])
+def test_base_converter_negative_numbers(number, from_base, to_base, function_to_test):
+ with block_int():
+ expected = reference_base_converter(number, from_base, to_base)
+ assert function_to_test(number, from_base, to_base) == expected
+
+
+@pytest.mark.parametrize(
+ "number,from_base,to_base", [("ff", 16, 10), ("FF", 16, 10), ("Ff", 16, 10)]
)
-def test_toboggan_p1(
- trees_map: List[List[int]], right: int, down: int, function_to_test
-) -> None:
- assert function_to_test(trees_map, right, down) == reference_toboggan_p1(
- trees_map, right, down
- )
+def test_base_converter_case_insensitive(number, from_base, to_base, function_to_test):
+ with block_int():
+ expected = reference_base_converter(number, from_base, to_base)
+ assert function_to_test(number, from_base, to_base) == expected
-def reference_toboggan_p2(trees_map: List[List[int]], slopes: Tuple[Tuple[int]]) -> int:
- """Reference solution (part 2)"""
- total = 1
- for right, down in slopes:
- total *= reference_toboggan_p1(trees_map, right, down)
- return total
+@pytest.mark.parametrize(
+ "number,from_base,to_base", [("42", 1, 10), ("42", 10, 17), ("42", 0, 0)]
+)
+def test_base_converter_invalid_bases(number, from_base, to_base, function_to_test):
+ with block_int():
+ with pytest.raises(ValueError):
+ reference_base_converter(number, from_base, to_base)
+ with pytest.raises(ValueError):
+ function_to_test(number, from_base, to_base)
@pytest.mark.parametrize(
- "trees_map, slopes",
+ "number,from_base,to_base",
[
- (
- trees_1,
- ((1, 1), (3, 1), (5, 1), (7, 1), (1, 2)),
- ), # 9354744432
- (
- trees_2,
- ((1, 1), (3, 1), (5, 1), (7, 1), (1, 2)),
- ), # 1574890240
+ ("2", 2, 10), # 2 not valid in base 2
+ ("G", 16, 2), # G not valid in base 16
+ ("9", 8, 2), # 9 not valid in base 8
],
)
-def test_toboggan_p2(
- trees_map: List[List[int]], slopes: Tuple[Tuple[int]], function_to_test
-) -> None:
- assert function_to_test(trees_map, slopes) == reference_toboggan_p2(
- trees_map, slopes
- )
+def test_base_converter_invalid_digits(number, from_base, to_base, function_to_test):
+ with block_int():
+ with pytest.raises(ValueError):
+ function_to_test(number, from_base, to_base)
+
+
+@pytest.mark.parametrize(
+ "number,from_base,to_base", [("", 2, 2), (" ", 2, 2), ("-", 2, 2)]
+)
+def test_base_converter_empty_or_invalid_input(
+ number, from_base, to_base, function_to_test
+):
+ with block_int():
+ with pytest.raises(ValueError):
+ function_to_test(number, from_base, to_base)
diff --git a/tutorial/tests/testsuite/helpers.py b/tutorial/tests/testsuite/helpers.py
index b30d05e7..5c7f17c5 100644
--- a/tutorial/tests/testsuite/helpers.py
+++ b/tutorial/tests/testsuite/helpers.py
@@ -766,6 +766,7 @@ def pytest_exception_interact(
outcome = (
TestOutcome.FAIL
if exc.errisinstance(AssertionError)
+ or exc.errisinstance(pytest.fail.Exception)
else TestOutcome.TEST_ERROR
)
self.tests[report.nodeid] = TestCaseResult(