0% found this document useful (0 votes)
22 views20 pages

Week 4

Uploaded by

Shashank S
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
22 views20 pages

Week 4

Uploaded by

Shashank S
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

1.

Solving a problem with a helper function


A common strategy for solving complex problems is to break them down into smaller, simpler
sub-problems. Each sub-problem can then be solved by a dedicated helper function.

Problem: Write a function is_valid_date(year, month, day) that returns True if the given date
is valid, and False otherwise.

This is a complex task. A date is valid if:


1.​ The year, month, and day are all positive integers.
2.​ The month is between 1 and 12.
3.​ The day is between 1 and the number of days in that specific month (which depends
on the month and whether the year is a leap year).

Our Friend's Proposed Solution:

Our friend wisely decides not to put all this logic into one giant function. Instead, they
propose a main function that delegates the hardest part—calculating the number of days in a
month—to a helper function.

Python

def num_days_in_month(year, month):


"""A helper function to calculate days in a given month."""
# (Implementation of this function would go here)
# For now, let's assume it works correctly.
if month == 2:
# We'll need another helper for this! is_leap(year)
# For simplicity, let's just hardcode for now.
return 28
elif month in {4, 6, 9, 11}:
return 30
else:
return 31

def is_valid_date(year, month, day):


"""
Checks if a given date is valid using a helper function.
"""
if not (year > 0 and 1 <= month <= 12 and day > 0):
return False

# Call the helper function to get the max valid day for this month.
max_day = num_days_in_month(year, month)

# Check if the given day is valid.


return day <= max_day
# --- Example Usage ---
print(f"Is 2024-09-30 a valid date? {is_valid_date(2024, 9, 30)}") # True
print(f"Is 2024-09-31 a valid date? {is_valid_date(2024, 9, 31)}") # False

This design is clean. The main function's logic is easy to read because the complex details
are encapsulated in the helper.

2. Calling a helper function


An analogy helps to understand the relationship between a main function and a helper
function.

The Head Chef Analogy 👨‍🍳


●​ Main Function (is_valid_date): The Head Chef responsible for creating a complex
main course.
●​ Helper Function (num_days_in_month): A Sous Chef who specializes in making
sauces.
●​ Arguments (year, month): The ingredients the Head Chef gives to the Sous Chef
(e.g., tomatoes, herbs, cream).
●​ Function Call: The Head Chef delegates the task: "Here are the ingredients, please
make the tomato sauce for me." The Head Chef then waits, free to work on other
parts of the dish.
●​ Return Value (max_day): The finished sauce. The Sous Chef hands it back to the
Head Chef.
●​ Using the Result: The Head Chef takes the finished sauce and incorporates it into
the main course.

The Head Chef doesn't need to know the specific recipe or steps the Sous Chef uses to
make the sauce. They just need to trust that when they provide the correct ingredients, they
will get the correct sauce back. This is called abstraction.

3. Tracing a call to the helper function


When a function calls another function, Python uses a structure called the call stack to keep
track of where it is. Each function call creates a new "frame" on top of the stack.

Let's visualize is_valid_date(2023, 4, 15) using a PythonTutor-style trace.


Step 1: The initial call to is_valid_date is made. A frame for it is created.

Global frame
(Global variables)

is_valid_date frame
year: 2023
month: 4
day: 15

Step 2: The code inside is_valid_date reaches the line max_day =


num_days_in_month(year, month). To resolve this, Python pauses is_valid_date and makes
a new call to the helper. A new frame is pushed onto the stack.

Global frame
(Global variables)

is_valid_date frame <-- PAUSED


year: 2023
month: 4
day: 15

num_days_in_month frame <-- ACTIVE


year: 2023
month: 4

Step 3: The code inside num_days_in_month runs. Since month is 4, it returns the value 30.

Step 4: The num_days_in_month function is finished. Its frame is destroyed, and the return
value (30) is sent back to the paused is_valid_date frame.

Global frame
(Global variables)

is_valid_date frame <-- ACTIVE AGAIN


year: 2023
month: 4
day: 15
max_day: 30 <-- The return value is assigned

Step 5: is_valid_date resumes execution. It calculates day <= max_day (15 <= 30), which is
True, and returns it. Its frame is then destroyed.
4. Definition of a leap year
The rules for determining a leap year are a classic programming exercise because they are
more complex than they first appear.

The Official Rules:

A year is a leap year if it is divisible by 4, with two exceptions.

●​ Rule 1: A year is a leap year if it is divisible by 4.


○​ e.g., 2024 is a leap year.
●​ Rule 2 (Exception): ...UNLESS the year is also divisible by 100.
○​ e.g., 1900 is divisible by 4 and by 100, so it is NOT a leap year.
●​ Rule 3 (Exception to the Exception): ...UNLESS the year is also divisible by 400.
○​ e.g., 2000 is divisible by 4, 100, and 400, so it IS a leap year.

Testing Your Understanding:

Is the year...

●​ 2023: No (not divisible by 4).


●​ 1996: Yes (divisible by 4, not by 100).
●​ 2100: No (divisible by 4 and 100, but not by 400).
●​ 2400: Yes (divisible by 4, 100, and 400).

5. Refuting our friend's code


Our friend attempts to write an is_leap function, but makes a common mistake by only
implementing the first rule.

Our Friend's Buggy Code:

Python

def is_leap(year: int) -> bool:


"""Checks if a year is a leap year (incorrectly)."""
# This only implements the "divisible by 4" rule.
return year % 4 == 0
Refuting the Code:

To refute this, we need a counterexample that exposes the missing logic. The year 1900 is
perfect for this.

●​ Counterexample: 1900
●​ Observed Output: is_leap(1900) calculates 1900 % 4 == 0, which is True. The
function returns True.
●​ Expected Output: According to the full rules, 1900 is not a leap year. The function
should have returned False.

Using GenAI to Correct the Code:

We can give the buggy code and the counterexample to an AI.

Prompt: "This Python function to check for leap years is buggy. It incorrectly returns True for
the year 1900. Please fix it and add comments explaining the full logic."

Likely GenAI Output:

Python

def is_leap(year: int) -> bool:


"""
Checks if a year is a leap year according to the Gregorian calendar rules.
"""
# A year is a leap year if it's divisible by 4,
# unless it's a century year not divisible by 400.
return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)

The AI correctly provides the full logical expression in a concise and accurate way.

6. Introduction to Recursion
Recursion is a powerful problem-solving technique where a function solves a problem by
calling itself on a smaller or simpler version of the same problem. This process continues
until it reaches a simple "base case" that can be solved directly without further recursion.

The Russian Doll Analogy

Imagine you have a set of Russian nesting dolls (Matryoshka dolls). Your goal is to find the
smallest, solid doll at the very center.

1.​ The Recursive Step: You open the largest doll. Inside, you don't find the final prize,
but a slightly smaller version of the same problem: another Russian doll. So, you
apply the same logic again: you open that doll.
2.​ The Base Case: You continue this process of opening smaller and smaller dolls.
Eventually, you will find a doll that cannot be opened. This is the solid center—the
base case. It's the point where the process stops.
3.​ Solving the Problem: By reaching the base case, you have solved the problem and
found the innermost doll.

A recursive function works the same way: it keeps breaking the problem down into smaller
versions of itself until it hits a base case it can solve immediately.

7. Infinite Recursion
The most critical part of a recursive function is the base case. A base case is a condition
that tells the recursion when to stop. Without a base case, a recursive function will call itself
forever, leading to infinite recursion.

A Recursive Function (Incorrect):

This function tries to count down from a number but forgets to include a stopping condition.

Python

def count_down(n):
print(n)
# The recursive step: call itself with a smaller number.
count_down(n - 1)
# There is no base case to tell it when to stop!

Visualizing the Error:

If we call count_down(2), PythonTutor would show the call stack growing uncontrollably:

1.​ A frame for count_down(2) is created. It prints 2.


2.​ It calls count_down(1). A new frame is created on top. It prints 1.
3.​ It calls count_down(0). A new frame is created. It prints 0.
4.​ It calls count_down(-1). A new frame is created. It prints -1.
5.​ This continues forever.

In reality, the computer doesn't have infinite memory for the call stack. After a few thousand
calls, Python will stop the program and raise a RecursionError: maximum recursion depth
exceeded. This prevents the program from crashing your entire system.
8. A correct recursive function
To fix infinite recursion, we must add a base case using an if statement. The base case
handles the simplest version of the problem and does not make another recursive call.

A Recursive Function (Correct):

Python

def count_down(n):
# BASE CASE: The simplest version of the problem.
# If n is zero or less, the countdown is over.
if n <= 0:
print("Blastoff!")
return # Stop the recursion.

# RECURSIVE STEP: The problem is not solved yet.


# Print the current number and solve the smaller problem.
else:
print(n)
count_down(n - 1)

# Let's call it!


count_down(3)

Output:

3
2
1
Blastoff!

Visualizing the Correct Process:

Let's trace count_down(2):

1.​ count_down(2) frame: 2 > 0, so it prints 2 and calls count_down(1).


2.​ count_down(1) frame: 1 > 0, so it prints 1 and calls count_down(0).
3.​ count_down(0) frame: 0 <= 0 is True. It hits the base case. It prints "Blastoff!" and
returns. This frame is destroyed.
4.​ Control returns to the count_down(1) frame. It has nothing left to do, so it finishes.
This frame is destroyed.
5.​ Control returns to the count_down(2) frame. It also finishes, and its frame is
destroyed.

The stack successfully grows and then unwinds to zero.


9. Using the default visualization
Tools like PythonTutor are fantastic for understanding recursion, but they have practical
limitations. To prevent web browsers from crashing, they limit the number of computational
steps they will visualize for any single run of code.

The Limitation:

If you try to visualize a recursive function with a large number of steps, the tool will stop
before the code is finished.

For example, if you paste the correct count_down function into PythonTutor and call it with a
large number like count_down(500), it will run for a while, showing the call stack getting
deeper and deeper. However, after a few hundred steps, the visualization will stop and
display a message similar to this:

"Stopped after 250 steps. To visualize more steps, edit the code to reduce
the amount of work."

This is not an error in your code. It's a safety feature of the visualization tool. It simply means
that your recursive call is too deep for the visualizer to trace to completion within its default
limits.

10. A non-recursive solution


Every problem that can be solved with recursion can also be solved without it, using
iteration (i.e., loops like for or while). For some problems, an iterative solution is simpler and
more efficient.

Here is an attempt at an iterative version of the countdown function. However, this version
contains a common bug.

A (Buggy) Non-Recursive Function:

Python

def count_down_iterative(n):
"""Counts down from n to 1 using a while loop."""
while n > 0:
print(n)
# The bug: The value of 'n' is never changed inside the loop.
print("Blastoff!")

The intention is for the while loop to run as long as n is positive, and for n to decrease on
each iteration. But the programmer forgot to include the line that decreases n (i.e., n = n - 1).
11. Refuting the buggy code
The bug in the non-recursive count_down_iterative function creates an infinite loop. This is
the iterative equivalent of infinite recursion.

Checking Inputs to Refute the Code:

We can refute this function by showing that for a valid input, it fails to terminate correctly.

●​ Counterexample: 3
●​ Trace:
1.​ n starts at 3.
2.​ The while n > 0 condition is checked. 3 > 0 is True, so the loop starts.
3.​ The code prints 3.
4.​ The loop finishes its first iteration and goes back to check the condition. n is
still 3.
5.​ 3 > 0 is still True. The loop runs again.
6.​ The code prints 3.
7.​ This repeats forever.
●​ Observed Output: The program prints 3 endlessly and never reaches the "Blastoff!"
line.
●​ Expected Output: The program should print 3, 2, 1, Blastoff!, and then terminate.

The function is refuted because it does not terminate, which is a fundamental requirement of
the countdown task.

12. Summary - Key Takeaways up to this point


1.​ Decomposition is Key: Helper functions are essential for breaking down complex
problems into smaller, more manageable parts. This improves code readability and
organization.
2.​ The Call Stack Manages Flow: Python uses a call stack to manage function calls.
When a function calls a helper, a new frame is pushed onto the stack. When the
helper returns, its frame is popped off.
3.​ Recursion is Self-Reference: A recursive function solves a problem by calling itself
with a simpler version of the input.
4.​ Base Cases are Mandatory: Every recursive function must have a base case—a
stopping condition—to prevent infinite recursion, which results in a RecursionError.
5.​ Iteration is an Alternative: Any recursive algorithm can also be implemented
iteratively using loops. Bugs in iterative code often manifest as infinite loops rather
than recursion errors.
6.​ Refutation Proves Incorrectness: Whether the solution is recursive or iterative, you
can refute it by finding a counterexample that leads to an incorrect result, a crash, or
a failure to terminate.
13. "Ask the client" Problems
In the real world, programming tasks often start as a simple request in natural language,
which can be surprisingly ambiguous. A good programmer's first job is not to code, but to
identify and resolve these ambiguities.

The Task: "Write a function remove_vowels that removes the vowels from a word."

This seems simple, but a programmer must consider several interpretations:


1.​ Definition of a Vowel: What letters count as vowels? Usually a, e, i, o, u. But what
about y? In "rhythm," y acts as a vowel, but in "yellow," it doesn't.
2.​ Case Sensitivity: Does "vowel" include uppercase A, E, I, O, U? If the input is
"Apple," should the output be "ppl"?
3.​ Output Casing: If case is ignored for removal, what should the case of the output
be? If the input is "Programming Is Fun," should the output be prgrmmng s fn or
Prgrmmng s Fn?
4.​ Edge Cases: What should the function do if the input is an empty string ("")? Or a
number? Or None?

Without clarification, five different programmers might write five different functions that all
technically "remove vowels" but behave differently in these specific cases.

14. Clarifying the task


To resolve ambiguity, you must ask clarifying questions. In a professional setting, you would
ask the client or product manager. In a programming contest or assignment, you might be
given a tool or a set of examples that act as an "oracle" to clarify the requirements.

Let's imagine an interface to clarify the remove_vowels task:

We can call a function clarify_task(input_word) to see the "correct" output for any input we're
unsure about.

Our Clarification Session:


1.​ Question: Is 'y' a vowel?
○​ Test: clarify_task("rhythm")
○​ Result: "rhythm"
○​ Conclusion: No, 'y' is not considered a vowel for this task.
2.​ Question: Is it case-sensitive? What happens to uppercase vowels?
○​ Test: clarify_task("Apple")
○​ Result: "ppl"
○​ Conclusion: It is case-insensitive. Both 'A' and 'a' are removed.
3.​ Question: What is the casing of the output?
○​ Test: clarify_task("Hello WORLD")
○​ Result: "hll wrld"
○​ Conclusion: The output is always converted to lowercase.
4.​ Question: How are empty strings handled?
○​ Test: clarify_task("")
○​ Result: ""
○​ Conclusion: An empty string results in an empty string.

By testing these specific inputs, we have created a much more precise specification for our
function.

15. Our friend's recursive function


Our friend wrote a recursive function to sum the digits of a number. Mentally tracing a
recursive function is a key skill.

Our Friend's Code:

Python

def sum_digits(n):
"""Recursively sums the digits of a non-negative integer."""
if n == 0:
return 0
else:
# Get the last digit (n % 10) and add it to the sum
# of the rest of the digits (n // 10).
return (n % 10) + sum_digits(n // 10)

Mentally Tracing sum_digits(254):


●​ sum_digits(254) calls sum_digits(25) and waits. It will eventually calculate 4 + [result
of sum_digits(25)].
○​ sum_digits(25) calls sum_digits(2) and waits. It will calculate 5 + [result of
sum_digits(2)].
■​ sum_digits(2) calls sum_digits(0) and waits. It will calculate 2 + [result
of sum_digits(0)].
■​ sum_digits(0) hits the base case and returns 0.
■​ The sum_digits(2) call receives 0, calculates 2 + 0, and returns 2.
○​ The sum_digits(25) call receives 2, calculates 5 + 2, and returns 7.
●​ The original sum_digits(254) call receives 7, calculates 4 + 7, and returns the final
answer, 11.

The logic seems sound for positive integers.


16. Tracing our friend's code
The sum_digits function works for positive integers, but what about negative ones? The
docstring is ambiguous. Let's trace sum_digits(-25) with PythonTutor to see what happens.

The Problem with Negative Numbers:

The behavior of the modulo (%) and floor division (//) operators can be non-intuitive for
negative numbers in Python.

●​ -25 % 10 is 5
●​ -25 // 10 is -3

Tracing sum_digits(-25):
1.​ Call 1: sum_digits(-25)
○​ -25 != 0.
○​ Returns (-25 % 10) + sum_digits(-25 // 10)
○​ Returns 5 + sum_digits(-3).
2.​ Call 2: sum_digits(-3)
○​ -3 != 0.
○​ Returns (-3 % 10) + sum_digits(-3 // 10)
○​ Returns 7 + sum_digits(-1).
3.​ Call 3: sum_digits(-1)
○​ -1 != 0.
○​ Returns (-1 % 10) + sum_digits(-1 // 10)
○​ Returns 9 + sum_digits(-1). UH OH!

The trace reveals that sum_digits(-1) calls itself with the exact same input, sum_digits(-1).
This is infinite recursion. The recursive step n // 10 does not bring all negative numbers
closer to the base case of 0.

17. Refuting our friend's code


The visualization from PythonTutor gives us everything we need to refute our friend's
sum_digits function.

Recognizing the Error:

The error is that the recursive step does not work as intended for negative integers. The
sequence of recursive calls for n = -25 is -25 -> -3 -> -1 -> -1 -> -1 ..., which never reaches
the base case n == 0.

The Refutation:
●​ Counterexample: Any negative integer, for example, -1.
●​ Observed Output: Infinite recursion, which eventually causes a RecursionError:
maximum recursion depth exceeded.
●​ Expected Output: The specification was ambiguous about negative numbers, but a
crash is certainly not the correct behavior. A reasonable expectation would be for the
function to handle the absolute value of the number, so sum_digits(-25) should return
7. Or, it should raise a ValueError for invalid input.

The function is refuted because it fails to terminate for an entire class of integer inputs.

18. Clarifying Questions: num_vowels


Let's return to the "count the number of vowels in a string" problem. Before coding, we must
establish a clear specification by asking clarifying questions.

The Task: Write num_vowels(s).

Our Questions and Established Rules:


1.​ "Which characters are considered vowels?"
○​ Clarification: The vowels are 'a', 'e', 'i', 'o', and 'u'. The letter 'y' is not a vowel.
2.​ "Is the count case-sensitive?"
○​ Clarification: No. The check should be case-insensitive, meaning 'A' counts
the same as 'a'.
3.​ "What is the expected output for an empty string?"
○​ Clarification: The number of vowels in "" is 0.
4.​ "What should happen if the input is not a string?"
○​ Clarification: For this problem, you can assume the input will always be a
string.

With these rules, we now have a concrete specification and can write a function with
confidence that it will meet the requirements.

19. Strings in Python


In Python, a string (str) is a sequence of characters. We need to understand two
fundamental properties to manipulate them: length and indexing.

Length of a String:

The built-in len() function returns the number of characters in a string.

Python

message = "Hello"
empty = ""

print(len(message)) # Output: 5
print(len(empty)) # Output: 0
Indexing Individual Letters:

You can access individual characters in a string using square brackets [] and an index.
Python uses 0-based indexing, meaning the first character is at index 0.

Python

s = "Python"
#Python
# 0 1 2 3 4 5 <-- Positive indices

#P y t h o n
# -6 -5 -4 -3 -2 -1 <-- Negative indices

●​ Positive Indexing (from the left):


○​ s[0] is 'P'
○​ s[4] is 'o'
●​ Negative Indexing (from the right):
○​ s[-1] is 'n' (the last character)
○​ s[-6] is 'P' (the first character)

Trying to access an index that doesn't exist (e.g., s[6]) will cause an IndexError.

20. Testing our understanding


Question: For any non-empty string s, what is the index of its last character?

Answer: len(s) - 1

Explanation:

Let's take the string s = "code".

●​ len(s) is 4.
●​ The characters are at indices 0, 1, 2, 3.
●​ The last character, 'e', is at index 3.
●​ This matches our formula: len(s) - 1 = 4 - 1 = 3.

This is a crucial relationship to remember. The valid indices for a string s are from 0 up to,
but not including, len(s).
21. Testing our understanding further
Question: What are the valid indices for the empty string, s = ""?

Answer: There are no valid indices.

Explanation:
●​ len(s) for the empty string is 0.
●​ The range of valid indices is 0 to len(s) - 1, which would be 0 to -1.
●​ Since there are no integers between 0 and -1 (inclusive), there are no valid positive
indices.
●​ Similarly, there are no valid negative indices.
●​ Attempting to access any index, such as s[0], will immediately raise an IndexError.

This is a critical edge case to handle in any code that involves string indexing.

22. String operations and methods


Here are four essential tools for working with strings in Python.
1.​ The in operator: A boolean operator that checks if a substring exists within another
string. It is case-sensitive.
2.​ Python

print('a' in 'team') # Output: True


print('sea' in 'seaside') # Output: True
print('A' in 'team') # Output: False (case-sensitive)
3.​
4.​
5.​ The .lower() method: A string method that returns a new string with all characters
converted to lowercase. The original string is unchanged.
6.​ Python

message = "Hello World"


lower_message = message.lower()
print(lower_message) # Output: "hello world"
print(message) # Output: "Hello World" (original is untouched)
7.​
8.​
9.​ String Slicing: Extracts a new string containing a portion of the original. The syntax
is s[start:stop], where stop is the first index not included.
10.​Python

s = "programming"
print(s[0:4]) # Output: "prog" (indices 0, 1, 2, 3)
print(s[4:]) # Output: "ramming" (from index 4 to the end)
print(s[:4]) # Output: "prog" (from the beginning up to index 4)
print(s[1:-1]) # Output: "rogrammin" (from index 1 to the second-to-last)
11.​
12.​
13.​The += operator (Concatenation): Appends a string to the end of another.
14.​Python

word = "auto"
word += "mobile" # word is now "automobile"
print(word)
15.​
16.​

23. Testing your understanding


Question: Look at the following function. What is its purpose? What does it do?

Python

def mystery_function(s: str) -> str:


"""What does this function do?"""
return s[::-1]

Answer: This function reverses a string.

Explanation:

The syntax s[::-1] is an extended form of string slicing. The full syntax is s[start:stop:step].

●​ By leaving start and stop blank, it defaults to the entire string.


●​ A step of -1 means to go through the string backward, one character at a time.

This slice is a common and concise "Pythonic" idiom for reversing any sequence, including
strings.

Python

print(mystery_function("hello")) # Output: "olleh"


24. Our friend's code: num_vowels
Based on our clarified specification, our friend has written a recursive function to count
vowels.

Our Friend's Recursive Function:

Python

def num_vowels(s: str) -> int:


"""
Recursively counts the number of vowels in a string (case-insensitive).
Vowels are 'a', 'e', 'i', 'o', 'u'.
"""
vowels = "aeiou"

# Base case: an empty string has no vowels.


if s == "":
return 0

# Recursive step:
# 1. Look at the first character of the string.
first_char = s[0].lower()
# 2. Get the rest of the string for the smaller recursive call.
rest_of_string = s[1:]

# 3. Check if the first character is a vowel.


if first_char in vowels:
# If it is, the total is 1 + the vowels in the rest of the string.
return 1 + num_vowels(rest_of_string)
else:
# If it's not, the total is just the vowels in the rest of the string.
return num_vowels(rest_of_string)

The logic appears solid: it has a correct base case and a recursive step that reduces the
problem size (by making the string one character shorter).
25. Refuting our friend's code
The prompt implies there is a bug, so let's introduce one. A common mistake in recursive
functions that process sequences is forgetting the base case.

A Buggy Version of the Code:

Python

def num_vowels_buggy(s: str) -> int:


"""A version without a base case."""
vowels = "aeiou"

# The base case is missing!

first_char = s[0].lower() # This line will cause an IndexError for ""


rest_of_string = s[1:]

if first_char in vowels:
return 1 + num_vowels_buggy(rest_of_string)
else:
return num_vowels_buggy(rest_of_string)

Refuting and Debugging the Code:

The bug will appear when the recursion reaches the simplest possible input: the empty
string.

●​ Counterexample: ""
●​ Trace: The initial call could be anything, e.g., num_vowels_buggy("cat").
○​ num_vowels_buggy("cat") calls num_vowels_buggy("at").
○​ num_vowels_buggy("at") calls num_vowels_buggy("t").
○​ num_vowels_buggy("t") calls num_vowels_buggy("").
○​ The call num_vowels_buggy("") then immediately tries to access s[0].
●​ Observed Output: IndexError: string index out of range.
●​ Expected Output: The function should have returned 0 for the empty string.

The Fix: The fix is to add the base case if s == "": return 0 at the very beginning of the
function to handle the empty string before any indexing is attempted.
26. Immutable Objects
An object is immutable if its internal state cannot be changed after it has been created. In
Python, fundamental types like str, int, and float are immutable.

Verifying str is immutable:

You cannot change a character within a string.

Python

s = "test"
# Let's try to change the first character to 'T'
try:
s[0] = 'T'
except TypeError as e:
print(e) # Output: 'str' object does not support item assignment

To "modify" a string, you must create a completely new string object.

Python

s = "test"
s_new = "T" + s[1:] # Creates a new string "Test"
print(s_new) # "Test"
print(s) # "test" (original is unchanged)

Verifying int and float are immutable:

This is less obvious but can be seen using the id() function, which gives the memory address
of an object.

Python

x = 10
print(f"Initial x: {x}, Memory ID: {id(x)}")

# This looks like we're changing x...


x=x+1

# ...but we actually created a new integer object (11)


# and made the name 'x' point to it.
print(f"Final x: {x}, Memory ID: {id(x)}") # The ID will be different!

You cannot change the integer object 10. Any operation that seems to modify it actually
creates a new integer object (11) and reassigns the variable name to this new object. The
same is true for floats. This behavior is fundamental to how Python manages memory and
data.
27. Summary - Key Takeaways
1.​ Ambiguity is the Enemy: Natural language specifications are often imprecise. A
programmer's first job is to "ask the client" by testing edge cases and asking
clarifying questions to create a concrete set of requirements.
2.​ Recursive Bugs Hide in the Base Case: Many recursive errors involve an incorrect
or missing base case, leading to infinite recursion (RecursionError) or a crash
(IndexError) when the simplest input (like an empty string or zero) is reached.
3.​ Trace Before You Trust: Mentally tracing your code's execution path, especially for
recursive calls and edge cases like negative numbers, is the most effective way to
find bugs before they happen.
4.​ Strings are Indexed, Sliced, and Immutable: Strings are 0-indexed sequences.
You access characters with s[i], extract substrings with s[start:stop], and use methods
like .lower() to create new, modified versions.
5.​ Immutability Means Creating New Objects: str, int, and float are immutable. Any
operation that appears to "change" an immutable object is actually creating a new
object in memory and reassigning the variable name to point to it. The original object
is untouched.

You might also like