diff --git a/.gitignore b/.gitignore
index f7e7a42..58ff0fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ __pycache__
*.cover
*.excalidraw
plann.txt
+exercises_with_notes
diff --git a/4__python_self_study_2/README.md b/4__python_self_study_2/README.md
index ab9fa8b..d78c266 100644
--- a/4__python_self_study_2/README.md
+++ b/4__python_self_study_2/README.md
@@ -1,9 +1,10 @@
# Python Self-Study 2
-To prepare for the rest of the workshops in this series, you should study these
+All of the workshop resources in this series will only use language features covered in [Python Self-Study 1](../2__python_self_study_1/). But as you become more comfortable programming you may want to explore further and practice solving harder challenges.
+
+To prepare for the next steps in your Python journey, you should study these
language features using your favorite resources in the language of your choice.
-You don't need to master them, just be familiar. Donβt forget to study with
-Predictive Stepping!
+You don't need to master them, just be familiar. Donβt forget to study using Documentation, Tests and Predictive Stepping!
- [ ] **Collections**: dictionaries, tuples, sets
- [ ] **Iteration**: `iter()`, `next()`, `enumerate()`
diff --git a/4_debugging/.assets/rubber-ducky.png b/4_debugging/.assets/rubber-ducky.png
new file mode 100755
index 0000000..584cacb
Binary files /dev/null and b/4_debugging/.assets/rubber-ducky.png differ
diff --git a/4_debugging/README.md b/4_debugging/README.md
index a4168fd..433f15d 100644
--- a/4_debugging/README.md
+++ b/4_debugging/README.md
@@ -17,12 +17,17 @@ different. But if you practice good habits, can avoid many bugs and be ready to
fix the bugs you can't avoid.
- [Learning Objectives](#learning-objectives)
-- [Debuggin](./debugging.md)
+ - [Fixing Errors](#fixing-errors)
+ - [Avoiding Bugs](#avoiding-bugs)
+ - [Fixing Bugs](#fixing-bugs)
+- [Errors vs. Bugs](#errors-vs-bugs)
+- [Understand, Plan, Experiment](#understand-plan-experiment)
+ - [Surprises](#surprises)
+ - [Experiments](#experiments)
+- [Philosophy of Debugging](./philosophy_of_debugging.md)
- [Prep Work](./prep_work.md)
- [Lesson Plan](./lesson_plan.md)
----
-
## Learning Objectives
Priorities: π₯π£π₯π (click for more info)
@@ -47,38 +52,136 @@ master all of the skills introduced in this workshop.
+### Fixing Errors
+
+- π₯ Understand the difference between an _error_ and a _bug_.
+ - **Error**: When the Python interpreter cannot execute your code and the program stops.
+ - **Bug**: When your program runs, but it's _behavior_ is not what you expected or wanted.
+- π₯ Use error messages and your debugger to find where errors occur in your code.
+- π₯ Search errors online using the _name_, the _message_ and your code for context.
+- π£ Understand the difference between _syntax_ and _semantic_ (or _runtime_) errors.
+- π£ Understand and explain why your error occurred: what about your code caused the program to stop?
+- π£ Fixing errors in your program.
+
+### Avoiding Bugs
+
+- π₯ Pair programming with someone you trust.
+- π₯ Always use the simplest and most understandable solution.
+ ([KISS](https://github.com/dwmkerr/hacker-laws#the-kiss-principle))
+- π₯ Develop your code one small step at a time, writing and running tests for
+ each change before moving on.
+- π₯ Have others read and review your code, they will find mistakes you missed
+ and think of improvements you wouldn't.
+- π₯ Write less code. Keep your end goal in mind and avoid writing any code that
+ is not _absolutely necessary_ to reach your goal.
+- π₯ Keep a _Bug Log_; Write down bugs you've encountered and how you fixed them.
+ This log will help you avoid making the same mistakes, and double as
+ inspiration for how to fix new bugs.
+- π₯ Know that someone else will always use your [differently than you'd like](https://www.youtube.com/watch?v=CfCiW4UhqLo).
+ - _test as many edge cases as possible!_
+- π₯ Don't trust AI without checking it's work! Always take the time to understand, test and debug code AI writes for you.
+
### Fixing Bugs
- π¦ You are not embarrassed to do some
[rubber duck debugging](https://rubberduckdebugging.com/).
-- π₯ You know that someone else will always use your program
- [in a way you didn't imagine](https://www.youtube.com/watch?v=CfCiW4UhqLo).
- π₯ You can study a program skeptically, always asking "_how can I break this
program?_".
- π£ You can identify steps of execution that surprise you. This will help
understand the gap between what a program _does_ do, and what it _should_ do.
- π£ You can clearly describe a bug by answering questions like these:
- on what line does the bug occur?
+ - what values were stored in memory when when the bug occurred?
- what language features are involved with the bug?
- what _should_ the program do? Name specific test cases and lines of code!
- What _does_ the program do? Name specific test cases and lines of code!
- π£ You can recognize these four types of bug: overt vs. covert, and persistent
vs. intermittent.
-- π₯ You can trace a program backwards from a surprising step to understand how
- it happened (either mentally or on paper, VSCode's debugger only goes
- forward).
+- π₯ You can trace a program backwards from a surprising step to understand how it happened
+ - Either mentally or on paper, almost all debuggers only go forward.
-### Avoiding Bugs in the Fist Place
+---
-- π₯ Pair programming with someone you trust.
-- π₯ Always use the simplest and most understandable solution.
- ([KISS](https://github.com/dwmkerr/hacker-laws#the-kiss-principle))
-- π₯ Develop your code one small step at a time, writing and running tests for
- each change before moving on.
-- π₯ Have others read and review your code, they will find mistakes you missed
- and think of improvements you wouldn't.
-- π₯ Write less code. Keep your end goal in mind and avoid writing any code that
- is not _absolutely necessary_ to reach your goal.
-- π₯ Keep a Bug Log; Write down bugs you've encountered and how you fixed them.
- This log will help you avoid making the same mistakes, and double as
- inspiration for how to fix new bugs.
+## Errors vs. Bugs
+
+Python mistakes (or _errors_) are when your program is not able to finish because you wrote code that Python cannot execute.
+Logic mistakes (or _bugs_) are when your code runs without an error, but does
+not do what you expected. It will take practice to recognize the difference, [this video](https://www.youtube.com/watch?v=tV0tQisuxPo) has some funny examples to get you started -
+You can think of errors vs. bugs in normal language:
+
+- **Error**: A sentence can have incorrect grammar, so others can't understand it - _Chair train did doing water_.
+- **bug**: A sentence can have correct grammar and still mean something different than you intended - _The panda eats shoots and leaves_.
+
+While fixing errors "only" requires a solid understanding of Python syntax and
+runtime. Fixing logic mistakes also requires an understanding of debugging strategies, testing and strategic thinking.
+
+---
+
+## Understand, Plan, Experiment
+
+Fixing bugs requires carefully understanding what your code _does do_ before trying to
+make it do what you _want_ it to do. After you understand the difference between
+what it _does_ do and what it _should_ do, you can make small
+experiments to discover how you can go from buggy code to working code.
+
+Becoming a master debugger will take lots of experience, the more bugs you've
+fixed the more solutions you know. Practicing this structured approach will help
+you learn the most from each bug you encounter.
+
+### What _does_ it do?
+
+When you're already thinking of what the program _should_ do, it's too easy to
+look at your buggy code and see what you _want_ to see. Not what's actually
+there. Clearing your mind and telling yourself "I know nothing" is the first
+step to understanding _exactly_ what the buggy code does and how it works.
+
+Here some questions you should practice asking and answering when debugging. If you can't answer these questions then fixing your bug will be luck, not skill!
+
+1. **What _should_ the code do?** The best way to understand what a function _should_ do is to write documentation and unit tests covering as many _edge cases_ as possible.
+2. **What _does_ it do?** Run your unit tests and study the output to understand the buggy function's _behavior_. Can you see a pattern in which tests pass and which ones fail? When a test fails what did the function _actually_ return, and how does this compare to the _expected_ value?
+ - You might want to write _documentation_ for your buggy function before you start debugging it! This will force you to understand it's _behavior_ very well before moving on.
+3. **How _does_ it work?** Spend the time to really understand the function's _strategy_ and _implementation_ before you start making any changes: Read the code, add comments for yourself, step through the function using paper or a debugger, ...
+
+### Surprises
+
+The whole point of debugging is that you _expect_ your code to do one thing, but
+it _actually_ does something different. That means there must be at least one
+line of code that surprises you! Finding the line(s) or step(s) that that surprise you is a huge step towards fixing your
+bug. If there is a line of code that surprises,
+then there's a good chance it has something to do with your bug.
+
+As you study the function, mark any lines of code or steps of execution that surprise you. Careful! You're not trying to fix the bug yet, you're still trying to understand the function _before_ you make any changes. Take notes on:
+
+- What values were stored in memory when the code surprised you?
+- What actually happened that surprised you?
+- What did you expect to happen instead?
+- ... anything else that jumps out at you. You never know which details might be important!
+
+### Experiments
+
+After you've understood what the code _should_ do, what the code _does_ do and
+you've found the lines that surprise you - it's time to start experimenting!
+This is where the real debugging begins.
+
+Fixing a bug doesn't happen by magic, and it usually doesn't happen in a
+single moment of genius. Fixing a bug happens in small steps using trial and
+error. You can think of it as a conversation with your code.
+
+- You ask your code if you found the answer by making a small change and running the tests.
+- The tests tells you if you found the answer by either passing or failing.
+
+There are may
+
+- Is something is missing? Try adding it in.
+- Is there something extra that shouldn't be there? Try removing it.
+- Is something int he wrong place? Try moving it somewhere else in the function.
+- Is it in the right place, but the logic isn't correct? Try making small changes to the logic and running the tests after each change.
+
+By taking notes on all the little experiments you make and what happened you can
+carefully fix your bugs without causing more problems. (and learn a lot on the
+way)
+
+---
+---
+
+[![rubber ducky](./.assets/rubber-ducky.png)](https://rubberduckdebugging.com)
diff --git a/4_debugging/common_mistakes.md b/4_debugging/common_mistakes.md
new file mode 100644
index 0000000..e69de29
diff --git a/4_debugging/exercises/1_buggy_tests/alternate_elements.py b/4_debugging/exercises/1_buggy_tests/alternate_elements.py
new file mode 100644
index 0000000..4643527
--- /dev/null
+++ b/4_debugging/exercises/1_buggy_tests/alternate_elements.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+A module for list manipulation focusing on alternating elements.
+This is part of the debugging exercise series focusing on buggy tests.
+
+Module contents:
+ - alternate_elements: Creates a new list with every other element
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+def alternate_elements(items: list) -> list:
+ """Returns a new list containing every other element from the input list.
+
+ Takes any list and returns a new list with elements at even indices
+ (0, 2, 4, etc.). The original list is not modified.
+
+ Parameters:
+ items: list, the input list to process
+
+ Returns -> list: new list containing every other element
+
+ Raises:
+ AssertionError: if input is not a list
+
+ Examples:
+ >>> alternate_elements([1, 2, 3, 4, 5])
+ [1, 3, 5]
+ >>> alternate_elements(['a', 'b', 'c'])
+ ['a', 'c']
+ >>> alternate_elements([])
+ []
+ """
+ assert isinstance(items, list), "input must be a list"
+ return items[::2]
diff --git a/4_debugging/exercises/1_buggy_tests/count_between.py b/4_debugging/exercises/1_buggy_tests/count_between.py
new file mode 100644
index 0000000..376b68f
--- /dev/null
+++ b/4_debugging/exercises/1_buggy_tests/count_between.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+A module for counting numbers within a range.
+This is part of the debugging exercise series focusing on buggy tests.
+
+Module contents:
+ - count_between: Counts how many numbers fall between two values
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+def count_between(numbers: list, lower: int, upper: int) -> int:
+ """Counts how many numbers in the list fall between lower and upper bounds.
+
+ The bounds are inclusive, meaning a number equal to the lower or upper
+ bound is counted. The numbers list can contain integers or floats.
+
+ Parameters:
+ numbers: list of numbers to check
+ lower: int, lower bound (inclusive)
+ upper: int, upper bound (inclusive)
+
+ Returns -> int: count of numbers between bounds
+
+ Raises:
+ AssertionError: if numbers is not a list or bounds aren't integers
+
+ Examples:
+ >>> count_between([1, 2, 3, 4, 5], 2, 4)
+ 3
+ >>> count_between([1.5, 2.5, 3.5], 2, 3)
+ 1
+ >>> count_between([], 0, 10)
+ 0
+ """
+ assert isinstance(numbers, list), "first argument must be a list"
+ assert isinstance(lower, int), "lower bound must be an integer"
+ assert isinstance(upper, int), "upper bound must be an integer"
+
+ count = 0
+ for num in numbers:
+ if lower <= num <= upper:
+ count += 1
+ return count
diff --git a/4_debugging/exercises/1_buggy_tests/count_vowels.py b/4_debugging/exercises/1_buggy_tests/count_vowels.py
new file mode 100644
index 0000000..99db9be
--- /dev/null
+++ b/4_debugging/exercises/1_buggy_tests/count_vowels.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+A module for counting vowels in a string.
+This is part of the debugging exercise series focusing on buggy tests.
+
+Module contents:
+ - count_vowels: Counts how many vowels are in a string
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+def count_vowels(text: str) -> int:
+ """Count the number of vowels (a,e,i,o,u) in a string.
+
+ Parameters:
+ text: str, the input string to check
+
+ Returns -> int: number of vowels in the text
+
+ >>> count_vowels("hello")
+ 2
+ >>> count_vowels("APPLE")
+ 2
+ >>> count_vowels("why")
+ 0
+ """
+ assert isinstance(text, str), "input must be a string"
+
+ vowels = "aeiou"
+ count = 0
+ for char in text.lower():
+ if char in vowels:
+ count += 1
+ return count
diff --git a/4_debugging/exercises/1_buggy_tests/remove_spaces.py b/4_debugging/exercises/1_buggy_tests/remove_spaces.py
new file mode 100644
index 0000000..6faad0c
--- /dev/null
+++ b/4_debugging/exercises/1_buggy_tests/remove_spaces.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+A module containing string manipulation functions for removing spaces.
+This is part of the debugging exercise series focusing on buggy tests.
+
+Module contents:
+ - remove_spaces: Removes all spaces from a string
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+def remove_spaces(text: str) -> str:
+ """Removes all spaces from a string.
+
+ This function takes any string input and returns a new string with all
+ space characters removed. It preserves all other characters including
+ numbers, punctuation, and special characters.
+
+ Parameters:
+ text: str, the input string to process
+
+ Returns -> str: the input string with all spaces removed
+
+ Raises:
+ AssertionError: if input is not a string
+
+ Examples:
+ >>> remove_spaces("hello world")
+ 'helloworld'
+ >>> remove_spaces(" spaces ")
+ 'spaces'
+ >>> remove_spaces("no spaces")
+ 'nospaces'
+ >>> remove_spaces("")
+ ''
+ """
+ assert isinstance(text, str), "input must be a string"
+ return text.replace(" ", "")
diff --git a/4_debugging/exercises/1_buggy_tests/tests/test_alternate_elements.py b/4_debugging/exercises/1_buggy_tests/tests/test_alternate_elements.py
new file mode 100644
index 0000000..b66444f
--- /dev/null
+++ b/4_debugging/exercises/1_buggy_tests/tests/test_alternate_elements.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Test module for alternate_elements function.
+Contains intentionally buggy tests for debugging practice.
+
+Test categories:
+ - Standard cases: typical lists with different lengths
+ - Edge cases: empty lists, single elements
+ - Defensive tests: wrong input types, assertions
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+import unittest
+
+from ..alternate_elements import alternate_elements
+
+class TestAlternateElements(unittest.TestCase):
+ """Test suite for the alternate_elements function - contains buggy tests!"""
+
+ # Standard test cases
+ def test_five_numbers(self):
+ """It should return every other number from a list of five"""
+ self.assertEqual(alternate_elements([1, 2, 3, 4, 5]), [2, 4])
+
+ def test_four_numbers(self):
+ """It should return every other number from a list of four"""
+ self.assertEqual(alternate_elements([1, 2, 3, 4]), [1, 3])
+
+ def test_strings(self):
+ """It should work with strings"""
+ self.assertEqual(alternate_elements(['a', 'b', 'c', 'd']), 'ac')
+
+ # Edge cases
+ def test_empty_list(self):
+ """It should return empty list for empty input"""
+ self.assertEqual(alternate_elements([]), [])
+
+ def test_single_element(self):
+ """It should return single element list for single element input"""
+ self.assertEqual(alternate_elements([1]), [])
+
+ def test_two_elements(self):
+ """It should return first element for two element input"""
+ self.assertEqual(alternate_elements([1, 2]), [1])
+
+ # Defensive tests
+ def test_none_input(self):
+ """It should raise AssertionError for None input"""
+ with self.assertRaises(AssertionError):
+ alternate_elements(None)
+
+ def test_string_input(self):
+ """It should raise AssertionError for string input"""
+ self.assertEqual(alternate_elements("hello"), ["h", "l", "o"])
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/4_debugging/exercises/1_buggy_tests/tests/test_count_between.py b/4_debugging/exercises/1_buggy_tests/tests/test_count_between.py
new file mode 100644
index 0000000..960be57
--- /dev/null
+++ b/4_debugging/exercises/1_buggy_tests/tests/test_count_between.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Test module for count_between function.
+Contains intentionally buggy tests for debugging practice.
+
+Test categories:
+ - Standard cases: typical number lists and ranges
+ - Edge cases: empty lists, equal bounds, non-integer numbers
+ - Defensive tests: invalid inputs, assertions
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+import unittest
+
+from ..count_between import count_between
+
+class TestCountBetween(unittest.TestCase):
+ """Test suite for the count_between function - contains buggy tests!"""
+
+ # Standard test cases
+ def test_simple_range(self):
+ """It should count numbers in a simple range"""
+ self.assertEqual(count_between([1, 2, 3, 4, 5], 2, 4), 1)
+
+ def test_no_matches(self):
+ """It should return 0 when no numbers in range"""
+ self.assertEqual(count_between([1, 2, 3], 4, 6), 0)
+
+ def test_all_match(self):
+ """It should count all numbers when all in range"""
+ self.assertEqual(count_between([2, 3, 4], 1, 3), 3)
+
+ # Edge cases
+ def test_empty_list(self):
+ """It should return 0 for empty list"""
+ self.assertEqual(count_between([], 1, 10), 0)
+
+ def test_equal_bounds(self):
+ """It should work when lower and upper bounds are equal"""
+ self.assertEqual(count_between([1, 2, 3, 2, 1], 2, 2), 0)
+
+ def test_float_numbers(self):
+ """It should work with float numbers in list"""
+ self.assertEqual(count_between([1.5, 2.0, 2.5, 3.0], 2, 3), 1)
+
+ # Defensive tests
+ def test_invalid_bounds(self):
+ """It should raise AssertionError if bounds aren't integers"""
+ with self.assertRaises(AssertionError):
+ count_between([1, 2, 3], 1.5, 3)
+
+ def test_reversed_bounds(self):
+ """It should work with reversed bounds"""
+ self.assertEqual(count_between([1, 2, 3, 4, 5], 4, 2), 3)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/4_debugging/exercises/1_buggy_tests/tests/test_count_vowels.py b/4_debugging/exercises/1_buggy_tests/tests/test_count_vowels.py
new file mode 100644
index 0000000..8a93f29
--- /dev/null
+++ b/4_debugging/exercises/1_buggy_tests/tests/test_count_vowels.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Test module for count_vowels function.
+Contains intentionally buggy tests for debugging practice.
+
+Test categories:
+ - Standard cases: typical number lists and ranges
+ - Edge cases: empty lists, equal bounds, non-integer numbers
+ - Defensive tests: invalid inputs, assertions
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+import unittest
+
+from ..count_vowels import count_vowels
+
+class TestCountVowels(unittest.TestCase):
+ """Test the count_vowels function - some tests are buggy!"""
+
+ def test_empty_string(self):
+ """It should return 0 for an empty string"""
+ self.assertEqual(count_vowels(""), 0)
+
+ def test_no_vowels(self):
+ """It should return 0 for strings without vowels"""
+ self.assertEqual(count_vowels("cry"), 1)
+
+ def test_all_vowels(self):
+ """It should count all vowels in a string"""
+ self.assertEqual(count_vowels("AUDIO"), 0)
+
+ def test_mixed_case(self):
+ """It should handle mixed case strings"""
+ self.assertEqual(count_vowels("Hello World"), 3)
+
+ def test_not_string(self):
+ """It should raise AssertionError for non-string input"""
+ with self.assertRaises(AssertionError):
+ count_vowels(123)
diff --git a/4_debugging/exercises/1_buggy_tests/tests/test_remove_spaces.py b/4_debugging/exercises/1_buggy_tests/tests/test_remove_spaces.py
new file mode 100644
index 0000000..354ae26
--- /dev/null
+++ b/4_debugging/exercises/1_buggy_tests/tests/test_remove_spaces.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Test module for remove_spaces function.
+Contains intentionally buggy tests for debugging practice.
+
+Test categories:
+ - Standard cases: typical inputs with spaces
+ - Edge cases: empty strings, only spaces, special characters
+ - Defensive tests: wrong input types, assertions
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+import unittest
+from ..remove_spaces import remove_spaces
+
+class TestRemoveSpaces(unittest.TestCase):
+ """Test suite for the remove_spaces function - contains buggy tests!"""
+
+ # Standard test cases
+ def test_single_word(self):
+ """It should return the same string if no spaces present"""
+ self.assertEqual(remove_spaces("hello"), "hello")
+
+ def test_two_words(self):
+ """It should remove a single space between words"""
+ self.assertEqual(remove_spaces("hello world"), "hello_world")
+
+ def test_multiple_words(self):
+ """It should remove all spaces between multiple words"""
+ self.assertEqual(remove_spaces("the quick brown fox"), " thequickbrownfox ")
+
+ # Edge cases
+ def test_empty_string(self):
+ """It should handle empty strings"""
+ self.assertEqual(remove_spaces(""), "")
+
+ def test_only_spaces(self):
+ """It should handle strings containing only spaces"""
+ self.assertEqual(remove_spaces(" "), " ")
+
+ def test_spaces_at_ends(self):
+ """It should remove spaces at start and end of string"""
+ self.assertEqual(remove_spaces(" hello "), "hello")
+
+ def test_special_characters(self):
+ """It should preserve all non-space characters"""
+ self.assertEqual(remove_spaces("hello, world!"), "helloworld")
+
+ def test_numbers_and_symbols(self):
+ """It should handle strings with numbers and symbols"""
+ self.assertEqual(remove_spaces("123 + 456"), "123+456")
+
+ # Defensive tests
+ def test_non_string_input(self):
+ """It should raise AssertionError for non-string input"""
+ with self.assertRaises(AssertionError):
+ remove_spaces(123)
+
+ def test_none_input(self):
+ """It should raise AssertionError for None input"""
+ with self.assertRaises(AssertionError):
+ remove_spaces(None)
+
+ def test_list_input(self):
+ """It should raise AssertionError for list input"""
+ self.assertEqual(remove_spaces(["hello", "world"]), "helloworld")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/4_debugging/exercises/2_buggy_functions/count_words.py b/4_debugging/exercises/2_buggy_functions/count_words.py
new file mode 100644
index 0000000..c5fa5c2
--- /dev/null
+++ b/4_debugging/exercises/2_buggy_functions/count_words.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+A module for counting the words in a string.
+This is part of the debugging exercise series focusing on buggy implementations.
+
+Module contents:
+ - count_words: Counts the number of words in a string
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+def count_words(text: str) -> int:
+ """Counts the number of words in a string.
+ Words are separated by spaces.
+
+ Parameters:
+ text: str, the input string
+
+ Returns -> int: number of words in the text
+
+ >>> count_words("hello world")
+ 2
+ >>> count_words("one")
+ 1
+ >>> count_words("")
+ 0
+ """
+ assert isinstance(text, str), "input must be a string"
+
+ return len(text.split(" "))
diff --git a/4_debugging/exercises/2_buggy_functions/find_longest.py b/4_debugging/exercises/2_buggy_functions/find_longest.py
new file mode 100644
index 0000000..8db3653
--- /dev/null
+++ b/4_debugging/exercises/2_buggy_functions/find_longest.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+A module for finding the longest item in a list.
+This is part of the debugging exercise series focusing on buggy implementations.
+
+Module contents:
+ - find_longest: Finds the longest string in a list
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+def find_longest(items: list) -> str:
+ """Returns the longest string from a list of strings.
+
+ If multiple strings tie for longest length, returns the first one found.
+ Empty strings are valid candidates for longest string.
+
+ Parameters:
+ items: list of strings to search
+
+ Returns -> str: the longest string found
+
+ Raises:
+ AssertionError: if input is not a list or contains non-strings
+ ValueError: if list is empty
+
+ Examples:
+ >>> find_longest(['a', 'bb', 'ccc'])
+ 'ccc'
+ >>> find_longest(['hello', 'hi', 'hey'])
+ 'hello'
+ >>> find_longest(['', 'a'])
+ 'a'
+ """
+ assert isinstance(items, list), "input must be a list"
+ if not items:
+ raise ValueError("list cannot be empty")
+
+ longest = items[0]
+ for item in items:
+ if len(item) > len(longest):
+ longest = item
+ return longest
diff --git a/4_debugging/exercises/2_buggy_functions/join_words.py b/4_debugging/exercises/2_buggy_functions/join_words.py
new file mode 100644
index 0000000..a01a50d
--- /dev/null
+++ b/4_debugging/exercises/2_buggy_functions/join_words.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+A module for string joining operations.
+This is part of the debugging exercise series focusing on buggy implementations.
+
+Module contents:
+ - join_words: Combines a list of words with a separator
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+def join_words(words: list, separator: str) -> str:
+ """Joins a list of words using the given separator.
+
+ Takes a list of words and combines them into a single string, placing
+ the separator between each word. Does not add separator at start or end.
+
+ Parameters:
+ words: list of strings to join
+ separator: str, the separator to place between words
+
+ Returns -> str: the joined string
+
+ Raises:
+ AssertionError: if words is not a list or separator is not a string
+
+ Examples:
+ >>> join_words(['hello', 'world'], ' ')
+ 'hello world'
+ >>> join_words(['a', 'b', 'c'], ',')
+ 'a,b,c'
+ >>> join_words([], '-')
+ ''
+ """
+ assert isinstance(words, list), "first argument must be a list"
+ assert isinstance(separator, str), "separator must be a string"
+
+ result = ""
+ for word in words:
+ result = result + separator + word
+ return result
diff --git a/4_debugging/exercises/2_buggy_functions/tests/test_count_words.py b/4_debugging/exercises/2_buggy_functions/tests/test_count_words.py
new file mode 100644
index 0000000..2be27ab
--- /dev/null
+++ b/4_debugging/exercises/2_buggy_functions/tests/test_count_words.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Test module for count_words function.
+Contains correct tests to help identify bugs in the implementation.
+
+Test categories:
+ - Standard cases: typical string lists
+ - Edge cases: empty strings, equal lengths
+ - Defensive tests: invalid inputs
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+import unittest
+
+from ..count_words import count_words
+
+class TestCountWords(unittest.TestCase):
+ """Test the count_words function"""
+
+ def test_empty_string(self):
+ """It should return 0 for empty string"""
+ self.assertEqual(count_words(""), 0)
+
+ def test_one_word(self):
+ """It should count a single word"""
+ self.assertEqual(count_words("hello"), 1)
+
+ def test_two_words(self):
+ """It should count two words"""
+ self.assertEqual(count_words("hello world"), 2)
+
+ def test_multiple_spaces(self):
+ """It should handle multiple spaces between words"""
+ self.assertEqual(count_words("hello world"), 2)
+
+ def test_only_spaces(self):
+ """It should return 0 for string with only spaces"""
+ self.assertEqual(count_words(" "), 0)
diff --git a/4_debugging/exercises/2_buggy_functions/tests/test_find_longest.py b/4_debugging/exercises/2_buggy_functions/tests/test_find_longest.py
new file mode 100644
index 0000000..f0651ee
--- /dev/null
+++ b/4_debugging/exercises/2_buggy_functions/tests/test_find_longest.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Test module for find_longest function.
+Contains correct tests to help identify bugs in the implementation.
+
+Test categories:
+ - Standard cases: typical string lists
+ - Edge cases: empty strings, equal lengths
+ - Defensive tests: invalid inputs
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+import unittest
+
+from ..find_longest import find_longest
+
+class TestFindLongest(unittest.TestCase):
+ """Test suite for the find_longest function"""
+
+ # Standard test cases
+ def test_increasing_lengths(self):
+ """It should find longest in increasing lengths"""
+ self.assertEqual(find_longest(['a', 'bb', 'ccc']), 'ccc')
+
+ def test_decreasing_lengths(self):
+ """It should find longest in decreasing lengths"""
+ self.assertEqual(find_longest(['ccc', 'bb', 'a']), 'ccc')
+
+ def test_mixed_lengths(self):
+ """It should find longest in mixed lengths"""
+ self.assertEqual(
+ find_longest(['hi', 'hello', 'hey', 'howdy']),
+ 'hello'
+ )
+
+ # Edge cases
+ def test_empty_string_first(self):
+ """It should handle empty string at start"""
+ self.assertEqual(find_longest(['', 'a', 'bb']), 'bb')
+
+ def test_empty_string_middle(self):
+ """It should handle empty string in middle"""
+ self.assertEqual(find_longest(['a', '', 'bb']), 'bb')
+
+ def test_all_empty(self):
+ """It should handle all empty strings"""
+ self.assertEqual(find_longest(['', '', '']), '')
+
+ def test_equal_lengths(self):
+ """It should return first string when multiple are longest"""
+ self.assertEqual(find_longest(['abc', 'def', 'ghi']), 'abc')
+
+ # Defensive tests
+ def test_empty_list(self):
+ """It should raise ValueError for empty list"""
+ with self.assertRaises(ValueError):
+ find_longest([])
+
+ def test_non_list_input(self):
+ """It should raise AssertionError for non-list input"""
+ with self.assertRaises(AssertionError):
+ find_longest("hello")
+
+ def test_non_string_items(self):
+ """It should raise AssertionError if list contains non-strings"""
+ with self.assertRaises(AssertionError):
+ find_longest(['hello', 42, 'world'])
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/4_debugging/exercises/2_buggy_functions/tests/test_join_words.py b/4_debugging/exercises/2_buggy_functions/tests/test_join_words.py
new file mode 100644
index 0000000..0bf8133
--- /dev/null
+++ b/4_debugging/exercises/2_buggy_functions/tests/test_join_words.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Test module for join_words function.
+Contains correct tests to help identify bugs in the implementation.
+
+Test categories:
+ - Standard cases: typical word lists with different separators
+ - Edge cases: empty lists, single words
+ - Defensive tests: invalid inputs, non-string items
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+import unittest
+
+from ..join_words import join_words
+
+class TestJoinWords(unittest.TestCase):
+ """Test suite for the join_words function"""
+
+ # Standard test cases
+ def test_two_words_space(self):
+ """It should join two words with space"""
+ self.assertEqual(join_words(['hello', 'world'], ' '), 'hello world')
+
+ def test_three_words_comma(self):
+ """It should join three words with comma"""
+ self.assertEqual(join_words(['a', 'b', 'c'], ','), 'a,b,c')
+
+ def test_words_with_dash(self):
+ """It should join words with dash"""
+ self.assertEqual(
+ join_words(['one', 'two', 'three'], '-'),
+ 'one-two-three'
+ )
+
+ # Edge cases
+ def test_empty_list(self):
+ """It should return empty string for empty list"""
+ self.assertEqual(join_words([], ','), '')
+
+ def test_single_word(self):
+ """It should return the word itself for single-item list"""
+ self.assertEqual(join_words(['hello'], ','), 'hello')
+
+ def test_empty_separator(self):
+ """It should join directly when separator is empty string"""
+ self.assertEqual(join_words(['a', 'b', 'c'], ''), 'abc')
+
+ def test_multi_char_separator(self):
+ """It should work with multi-character separators"""
+ self.assertEqual(
+ join_words(['hello', 'world'], ' :: '),
+ 'hello :: world'
+ )
+
+ # Defensive tests
+ def test_non_list_input(self):
+ """It should raise AssertionError for non-list input"""
+ with self.assertRaises(AssertionError):
+ join_words("hello", ',')
+
+ def test_non_string_separator(self):
+ """It should raise AssertionError for non-string separator"""
+ with self.assertRaises(AssertionError):
+ join_words(['a', 'b'], 123)
+
+ def test_non_string_items(self):
+ """It should raise AssertionError if list contains non-strings"""
+ with self.assertRaises(AssertionError):
+ join_words(['hello', 42, 'world'], ' ')
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/4_debugging/exercises/3_figure_it_out/reverse_words.py b/4_debugging/exercises/3_figure_it_out/reverse_words.py
new file mode 100644
index 0000000..8c5c5cb
--- /dev/null
+++ b/4_debugging/exercises/3_figure_it_out/reverse_words.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+A module for reversing a sentence word-by-word.
+This function may be buggy!
+
+Module contents:
+ - reverse_words: Reverses a sentence word-by-word
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+def reverse_words(text: str) -> str:
+ """Reverses the order of words in a string.
+ Words are separated by single spaces.
+
+ Parameters:
+ text: str, the input string
+
+ Returns -> str: string with words in reverse order
+
+ >>> reverse_words("hello world")
+ 'world hello'
+ >>> reverse_words("one")
+ 'one'
+ >>> reverse_words("")
+ ''
+ """
+ assert isinstance(text, str), "input must be a string"
+
+ words = text.split(" ")
+ result = ""
+
+ for word in words:
+ result = " " + word + result
+
+ return result
diff --git a/4_debugging/exercises/3_figure_it_out/tests/test_reverse_words.py b/4_debugging/exercises/3_figure_it_out/tests/test_reverse_words.py
new file mode 100644
index 0000000..2006854
--- /dev/null
+++ b/4_debugging/exercises/3_figure_it_out/tests/test_reverse_words.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Test module for reverse_words function.
+Contains correct tests to help identify bugs in the implementation.
+ careful! these tests may have bugs too.
+
+Test categories:
+ - Standard cases: typical string lists
+ - Edge cases: empty strings, equal lengths
+ - Defensive tests: invalid inputs
+
+Created on 2024-12-06
+Author: Claude AI
+"""
+
+import unittest
+
+from ..reverse_words import reverse_words
+
+class TestReverseWords(unittest.TestCase):
+ """Test the reverse_words function"""
+
+ def test_empty_string(self):
+ """It should return empty string for empty input"""
+ self.assertEqual(reverse_words(""), "")
+
+ def test_one_word(self):
+ """It should return the same string for one word"""
+ # BUG: Test doesn't account for extra space bug
+ self.assertEqual(reverse_words("hello"), "hello")
+
+ def test_two_words(self):
+ """It should reverse two words"""
+ # BUG: Test expects wrong output format
+ self.assertEqual(reverse_words("hello world"), "world,hello")
+
+ def test_multiple_spaces(self):
+ """It should handle multiple spaces"""
+ self.assertEqual(reverse_words("hello world"), "world hello")
+
+ # write more tests!
diff --git a/4_debugging/exercises/README.md b/4_debugging/exercises/README.md
index e69de29..adb9d5b 100644
--- a/4_debugging/exercises/README.md
+++ b/4_debugging/exercises/README.md
@@ -0,0 +1,9 @@
+# Debugging: Exercises
+
+In these exercises you are given buggy functions and unit tests generated with different LLMs.
+
+Learning to test, understand and debug code is now more important than ever! LLMs can generate lots of code very quickly, but you still need to clearly tell them what to write (_documentation_), verify the code work correctly (_testing_), and modify the code when it doesn't (_debugging_).
+
+1. [Buggy Tests](./1_buggy_tests/): The functions (should) work, the unit tests are buggy
+1. [Buggy Functions](./2_buggy_functions/): The functions are buggy, the unit tests (should) work.
+1. [Figure it Out](./3_figure_it_out/): The functions or the tests can be buggy, you're on your own!
diff --git a/4_debugging/lesson_plan.md b/4_debugging/lesson_plan.md
index e69de29..cb5ab03 100644
--- a/4_debugging/lesson_plan.md
+++ b/4_debugging/lesson_plan.md
@@ -0,0 +1,163 @@
+class: middle, center
+
+
+
+# Debugging
+
+
+
+
+
+---
+
+class: middle, center
+
+## Knowledge Sharing: _Slido_
+
+---
+
+class: middle
+
+## Agenda
+
+- **Learning Objectives**
+
+- **Errors vs. Bugs**
+
+- **Avoiding Bugs**
+
+- **Fixing Bugs**
+
+- **Breakout Groups: _./exercises_**
+
+- **Discussion + Q&A**
+
+---
+
+class: middle
+
+## Learning Objectives
+
+- You can describe the difference between an _error_ and a _bug_
+
+- Remember: It's easier to avoid bugs than to fix them!
+
+- Good habits are the key to avoiding and fixing bugs
+
+- Buggy code isn't "broken", it behaves differently than you _expected_
+
+ - Take time to understand the code before making changes
+
+- You understand the value of careful habits when debugging:
+
+ - Write good tests and run them frequently
+
+ - Make one small change at a time, then run and study the tests
+
+ - Keep a Bug Log!
+
+- For self-study: _[objectives in the README](./README.md#learning-objectives)_
+
+---
+
+class: middle
+
+## Errors vs. Bugs
+
+- **Errors**: When your program stops running because of a Python mistake
+
+- **Bugs**: When your program runs, but does something unexpected
+
+---
+
+class: middle
+
+## Avoiding Bugs: _good habits_
+
+- Pair Programming
+
+- Have someone else proofread your code whenever possible
+
+- Keep a _Bug Log_ and reference it when you write code
+
+- Write lots of tests and run them after every small change
+
+- Try writing tests _before_ writing code!
+
+ - _we'll see this in the next workshops_
+
+---
+
+class: middle
+
+## Avoiding Bugs: _KISS*_
+
+Debugging is twice as hard as writing the code in the first place. Therefore,
+if you write the code as cleverly as possible, you are, by definition, not
+smart enough to debug it.
+
+[Brian Kernighan](https://github.com/dwmkerr/hacker-laws#kernighans-law)
+
+
+
+
+
+> \* Keep it simple, stupid.
+
+---
+
+class: middle
+
+## Fixing Bugs
+
+- Be very clear what the code _should_ do
+
+ - Documentation
+ - Unit Tests
+
+- Take your time understanding what the code _does_ do
+
+ - Studying test output
+ - Predictive stepping
+
+- Find the line(s) or step(s) that surprise you
+
+- Make one simple, experimental change at a time
+
+ - What do you expect should happen now?
+ - Run the tests and study the output
+ - What actually happened?
+
+---
+
+class: middle, center
+
+### Fixing Bugs: _Rubber Ducky_
+
+[![rubber ducky](./.assets/rubber-ducky.png)](https://rubberduckdebugging.com)
+
+---
+
+class: middle, center
+
+## Breakout Groups
+
+### _./exercises_
+
+---
+
+class: middle, center
+
+## Discussion + Q & A
+
+---
+
+class: middle, center
+
+# Thank You
+
+
+
+
+
+---
diff --git a/4_debugging/debugging.md b/4_debugging/philosophy_of_debugging.md
similarity index 98%
rename from 4_debugging/debugging.md
rename to 4_debugging/philosophy_of_debugging.md
index e5100f9..d5977ed 100644
--- a/4_debugging/debugging.md
+++ b/4_debugging/philosophy_of_debugging.md
@@ -1,4 +1,4 @@
-# Debugging
+# Philosophy of Debugging
> Adapted from Nick Parlante:
> [Debugging](https://web.stanford.edu/class/archive/cs/cs106a/cs106a.1184//handouts/9%20-%20Debugging.pdf)
@@ -140,14 +140,14 @@ being systematic, you always know which part of the field you've checked.
Identify the line which corresponds to the symptom. This is typically is very
simple. For the above example the symptom corresponds to the line where the
-variable went out of bounds or to the printfs which produced the garbage output.
+variable went out of bounds or to the `print`s which produced the garbage output.
The incorrect behavior will correspond to a variable with a bad value. Identify
the variable(s) with bad values. For this example, suppose that the program
crashes on line 112 because i is -1 and tries to index an array.
The critical question is: where did i get its value? There are basically three
ways a variable can get a value: the variable appears on the left hand side of a
-=, a reference to the variable is passed to somebody who changes it, or there is
+`=`, a reference to the variable is passed to somebody who changes it, or there is
a bad pointer reference somewhere which is accidentally scribbling on the
variable. Arguably a fourth way is if the variable is never initialized and so
gets a random value. Writing your code with good style: sensibly used
diff --git a/4_debugging/prep_work.md b/4_debugging/prep_work.md
index 4f378be..fada9e0 100644
--- a/4_debugging/prep_work.md
+++ b/4_debugging/prep_work.md
@@ -1,58 +1,5 @@
-# Lesson Plan
+# Prep Work
-## Workshop Overview _(all together)_
+Nothing new here technically! Debugging is a way of thinking, some good habits, and some helpful strategies. This workshop will build on the skills, tools and language features from the previous workshops to help you be more comfortable fixing bugs.
-The workshop instructor will introduce the main concepts of this workshop:
-
-- Introduce the workshop's [learning objectives](./README.md#learning-objectives).
-- Demonstrate _predictive stepping_ in a debugger.
-
-## Practice Predictive Stepping _(small groups)_
-
-Have one person in your group share their screen, then practice asking and
-answering these questions as you study the programs in
-[/0_stepping_through](./0_stepping_through):
-
-Before each step:
-
-- Which line will run next?
-- What will change in memory? (_variables_ and _callstack_!)
-
-After each step:
-
-- Were your predictions correct?
-- If now, why? What did you miss when you made your prediction?
-
-## The Debugging Mindset _(all together)_
-
-- Each group will have 1 minute to share:
- - One thing they couldn't understand
- - One interesting thing they learned
-- Introduce the next exercises
-
-## Describe Bugs _(small groups)_
-
-Try to find the bug in each program of
-[/1_describing_bugs](./1_describing_bugs/):
-
-## Discussion & Introduce Next Exercise _(all together)_
-
-- Each group will have 1-2 minutes to share:
- - One thing they couldn't figure out
- - One surprising thing they learned
- - One thing they'd like to discuss with the full class
-- Discuss how you approach code differently when you expect it has a bug, and
- why it's important to always think this way.
-- Introduce the next exercises.
-
-## Fixing Bugs _(small groups)_
-
-Find and fix as many bugs as you can in [/2_fixing_bugs](./2_fixing_bugs)!
-
-## Discussion _(all together)_
-
-- Each group will have 2-3 minutes to share:
- - One thing they couldn't figure out
- - One surprising thing they learned
- - One thing they'd like to discuss with the full class
-- Discuss!
+The best way to prepare for the _Debugging_ workshop is to read through the [Debugging README](./README.md) and [Philosophy of Debugging](./philosophy_of_debugging.md), then continue practicing what we covered in the _Predictive Stepping_ and _Documenting and Testing_ workshops.
diff --git a/suggested_study/python.md b/suggested_study/python.md
index 980cd15..0cb90fa 100644
--- a/suggested_study/python.md
+++ b/suggested_study/python.md
@@ -33,6 +33,13 @@
- `$ python -m trace -c path/to/file.py`
- [The VSCode Debugger](https://code.visualstudio.com/docs/python/debugging)
+## Errors
+
+- [Syntax vs. Semantics in Programming Languages (Udacity)](https://www.youtube.com/watch?v=vP-mn62EF0o)
+- [Syntax, Runtime and Logical Errors in Python (Learn Learn Scratch Tutorials)](https://www.youtube.com/watch?v=ToPP5UGgJUM)
+- [Syntax, Runtime and Semantic Errors (Omar Shaaban)](https://www.youtube.com/watch?v=bN0oGYD3z60) - _"Semantic errors" in this video are what we call Bugs_
+- [Python Error Types (Homawccc)](https://www.youtube.com/watch?v=Kf6dvup_6Mo)
+
## Tutorials and Courses
- [Computational Core: Introduction to Python (KSU)](https://textbooks.cs.ksu.edu/intro-python/): A great course with live exercises, program visualization (_Python Tutor_) and worked examples.