Bugs on the Range

Jed Rembold

September 12, 2025

Announcements

  • PS1 feedback went out!
    • Didn’t get an email? From your PS1 repo, click the “Pull Requests” tab at the top, and then select the “Feedback” pull request. Then scroll down a little.
    • Most damaging mistake? Failure to fully read the directions.
  • You have Problem Set 2 due on Monday!
    • You have absolutely everything you need to do all the problems after today
  • I’ll be at the SCRP Presentations this afternoon between around 2 and 4, but you could maybe catch me in my office between 1-2 while I eat lunch
  • Polling: polling.jedrembold.prof

Shorting the Circuit

  • Python evaluates and and or operators using a strategy called short-circuit mode
  • Only evaluates the right operand if it actually needs to
    • Example: if n=0, then the x % n == 0 is never actually checked in the statement

      n != 0 and x % n == 0

      since n != 0 already is False and False and anything is always False

  • Can use short-circuit to prevent errors: the above x % n == 0 statement would have erred out if n=0

Review Question

What value is printed when the code below runs?

  1. True
  2. False
  3. 5
  4. This would give an error
A = 10
B = 4
C = 1
A *= B
if A > 40 and C != 10 % 4:
    print(B+C)
elif A ** B <= C:
    print(A * C)
else:
    print(A < B or not (C == 10 // 4))

Home on the Range

The range() iterable

  • Need an easy way to produce or describe a range of numeric values
  • The built-in range() function handles this and produces the needed iterable object
  • Takes 1, 2, or 3 arguments:
    • Start (default 0): where to start the sequence at
    • Stop (mandatory): the sequence ends just below this value (does not include it!)
    • Step (default 1): what value the sequence counts by

Be careful, the range function will stop one step before the final stop value.

For Loop Anatomy

  • We have already seen for loops in Karel, but we can expand on them a bit now

  • The more general syntax of a for loop looks like:

    for |||variable||| in |||sequence|||:
        |||code to loop over|||
    • |||variable||| is a variable name that will be assigned every value in the sequence over the course of the loop
    • |||sequence||| is any expression that produces an object which supports iteration
      • We’ve already seen range() fulfills this role, and thus produces an iterable sequence
      • Any sequence works though, and we’ll see examples of another type of sequence next week

For ranging examples

  • Providing just a stop argument:

    for n in range(5):
        print(n)
  • Providing a start and stop:

    for n in range(1,11):
        print(n)
  • Providing a start, stop, and step:

    for n in range(10,0,-1):
        print(n)


0
1
2
3
4
1
2
3
4
5
6
7
8
9
10
10
9
8
7
6
5
4
3
2
1

Understanding Check

Which of the below blocks of code would print something different from the others?

for n in range(10):
    if n % 2 == 0:
        if n <= 10:
            print(n)
for k in range(0,10):
    if not (k % 2 > 0):
        print(k)
j = 0
while j < 10:
    print(j)
    j += 2
for i in range(0,10,2):
    if i > 0:
        print(i)

Algorithmic Approaches

Algorithms

  • Recall that when approaching a computation problem, you must have an algorithm designed before you start coding
  • An algorithm is a problem-solving strategy, and should be:
    • Clear and unambiguous, in the sense that it is understandable and leaves no gaps
    • Effective, in the sense that each step is actually possible
    • Finite, in the sense that it ends at some point
  • You need to come up with an algorithm before you start coding!

Creating your own Algorithms

  • Some useful hints to keep in mind when constructing your own algorithms:
    • Think about how you would solve the problem without a computer. You can’t write code if you don’t understand what you want the computer to do.
    • Computers are fast! Brute force methods are often very viable, or at least a good starting point.
    • Try to use tools and programming patterns you have already seen. It is often far easier to write programs by assembling pieces from code you have already seen than writing each program entirely from scratch.
      • Common patterns we have already seen include: looping over sequences, and using variables to track/control a loop
    • Recognize that the program you write is highly unlikely to work the first time
      • Errors can occasionally be in your algorithms
      • More often, early on, errors are in your translating of the algorithm into Python (the implementation)

Example: Greatest Factor

  • Suppose we wanted to write a function to compute the greatest factor of a provided number (not including the number itself)

  • Algorithm:

    • Check if factor by seeing if remainder 0
    • Brute force – check all smaller values to see if factor
    • Start at top and work down, means first found is the greatest
    def greatest_factor(num):
        """Finds the greatest factor of a number."""
        for i in range(num-1,0,-1):
            if num % i == 0:
                return i

Purging Debugs

Debugging

If debugging is the process of removing software bugs, then programming must be the process of putting them in.

Edsger W. Dijkstra

  • Everyone makes mistakes when writing code
  • A core skill then is in efficiently finding the bugs that you introduce
  • We’ll spend the rest of today looking at some good practices
    • As always though, practice makes perfect

Strategy #1

Concentrate on what your program IS doing, instead of what it SHOULD be doing.

  • It is impossible to find code that is missing
  • Instead focus on determining what your program is doing, or why it is behaving a certain way
  • Only once you understand what it is currently doing can you entertain thinking about how to change it productively

Strategy #2

Let Python help you: print or log the state of different variables.

  • Many errors are caused by you expecting a variable to have some content that it doesn’t have
  • Get Python to help you by adding print statements to print those variables out
  • Add print statements in blocks of code that you aren’t sure are being accessed to see if you see a message

Strategy #3

Stop and read. The documentation. The error messages.

Parsing Error Messages

  • Start at the bottom! That is where the general type of error and a short description will show up.
  • Want to know where it happened? Look one line up from that.
    • Will show a copy of the line where the error occurred
    • One line up from that will include the line number
  • Want nicer error messages?
    • The rich library offers some very pretty error messages: install with pip install rich

    • At the top of your code, then include:

      from rich.traceback import install
      install(show_locals=True)

Strategy #4

Use PythonTutor or a debugger to track EXACTLY what is happening in your program.

Strategy #5

Don’t make random changes to your code in the hopes that it will miraculously start working.

  • Making random changes is easy, fast, and doesn’t require a lot of thought
  • Unfortunately it is, at best, a wildly inefficient method of debugging, and at worst, actively detrimental
  • If you don’t know what you need to fix yet, you either haven’t:
    • Defined what you are attempting to do clearly enough, or
    • Understood / tracked your program well enough to know what it is currently doing

Strategy #6

Talk it out.

  • Explaining things verbally, in plain English, uncovers a shocking amount of misconceptions or mistakes
  • Find someone to talk at about your programming issues
    • It isn’t even important that they understand how to code, or even can talk back to you (though that might help in some cases)
    • Rubber Duck Debugging is where a software developer explains an issue out loud to an inanimate rubber duck

Strategy #7

Test your code as you go! Either manually or automatically.

  • Know that everyone makes mistakes. The longer you go without testing that something in your program works, the harder it is to find where the mistake eventually is.
  • Write code that you test in small pieces as you go
    • Decomposition into smaller functions is great for this: test each function individually as you go
    • In the projects we try to construct the Milestones for this exact same purpose

Assertions

  • You can use Python’s assert statement to write test functions, which take the form:

    assert |||condition|||

    where |||condition||| is any operation that returns a True or False

  • Assert statements “expect” a condition to yield a True, and if they do, nothing happens

    • No news is good news in this case!
  • If an assert condition evaluates to False, an error is raised

  • Naming your test functions starting with the word test_ will make them automatically discoverable by other tools

Testing Example

  • Suppose we wanted to write some checks of the greatest_factor function from earlier
def test_greatest_factor():
    """ Runs several tests on the function greatest_factor """
    assert greatest_factor(10) == 5
    assert greatest_factor(7) == 1
    assert greatest_factor(51) == 17
    assert greatest_factor(9) == 3
// reveal.js plugins