Turtle coordinate assertion bug

Question:
I am building a miniproject using the turtle module . The script works really well until i add a list of assertion tests. Does anyone know why the turtle keeps moves to different coordinates before going to the input coordinate Δy?
Repl link:
https://replit.com/@jonathanessombe/new-attempt#main.py

import turtle

Thomas = turtle.Turtle()
Thomas.shape("turtle")
Thomas.penup()

screen = turtle.Screen()
screen.setup(1000, 1000)

Δy = (input("How high do you want Thomas to go Vertically?")).strip()


def Thomas_task(Δy):
    try:
        if type(int(Δy)) != int:
            raise ValueError
        if int(Δy) > 200 or int(Δy) < -200:
            Thomas.fd(-10)
            return "    Hey, thats not a number between 200 and -200!"
        Thomas.goto(0, int(Δy))
        if 150 < int(Δy) <= 200:
            return "    This is very high!"
        if 100 <= int(Δy) <= 150:
            return "    This is high!"
        if 0 <= int(Δy) < 100:
            return "    This is high but not high enough!"
        if 0 > int(Δy) >= -100:
            return "    This is low but not low enough!"
        if -100 >= int(Δy) > -150:
            return "    This is low!"
        if -150 > int(Δy) > -200:
            return "    This is very low!"
    except ValueError:
        Thomas.fd(-10)
        return "    Please enter a valid integer!"


assert Thomas_task("uolkhdaw") == "    Please enter a valid integer!"
assert Thomas_task("300") == "    Hey, thats not a number between 200 and -200!"
assert Thomas_task("20") == "    This is high but not high enough!"
assert Thomas_task("140") == "    This is high!"
assert Thomas_task("160") == "    This is very high!"
assert Thomas_task("-90") == "    This is low but not low enough!"
assert Thomas_task("-140") == "    This is low!"
assert Thomas_task("-180") == "    This is very low!"
assert Thomas_task("-sadw") == "    Please enter a valid integer!"

Thomas.write(Thomas_task(Δy))

Before I take a look at your code, consider avoiding special non-alphanumeric characters in your variable names (because it can interfere with some python versions and interpreters). Try using delta_y or y instead.

1 Like

Hello, this bug is caused by “side effects” from your assert statements.
Remember, Thomas is a global variable and it is shared (there is only one Thomas turtle).
Basically, the function Thomas_task moves Thomas. Always. Even if it is just an assert statement. (Also, Thomas does not automatically reset to a position when the function returns, which is another problem.)

So, when you put down a bunch of assert statements that call Thomas_task, Thomas moves around a bunch.
(Also, Thomas is in the wrong position when Thomas_task is called at the end with Thomas.write because of side effects from the previous assert statements.)

(there are some other problems with your code but they don’t affect the functionality)

1 Like

What do you mean by Thomas is in the wrong position when Thomas_task is called?

What other problems are there with my code that don’t affect it’s functionality?

First question:
Let’s say that right before the Thomas.write line, you put down 5 assert statements that all say:

assert Thomas_task('300')

and then you input '300' into the program for Δy.
If I understand this correctly, then Thomas is now much to the left of the screen because the function is moving him left a lot, and if you keep on calling Thomas_task like this, then Thomas will eventually go off the screen. (but Thomas’s position is reset when Thomas_task is called with a valid input).


Second question:
In python, there are many “best practices” to use and “bad practices” to avoid that you eventually learn.
Here is a large list of ways your code could be slightly improved (but are all optional):

  • As mentioned by python660, always try to use only ASCII characters (no special symbols) in variable names.
  • In Thomas_task function, you are repeatedly converting Δy to int with int(Δy). Instead, do it just once, with a variable, e.g.: Δy = int(Δy)
  • In Thomas_task, do not wrap the entire function in a try-except block. Instead, wrap only the code that might raise an error. Also, avoid raising exceptions as control flow: you raise ValueError to intentionally be caught to go to the invalid case (but you can remove the check entirely as explained below).
  • There is no need to check if int(Δy) is an int because it is guaranteed to be an int (if Δy is not a valid int then ValueError is raised anyway).
  • Always use isinstance(x, int) instead of type(int(x)) is int (though you don’t even need the check in your code)
  • Reset the position of Thomas at the start of the function to prevent bugs from previous calls affecting later calls.
  • (stylistic) In the comparisons in Thomas_task function, you flip the direction of < and <= for negatives. Keep the comparisons in the < direction. Also, be consistent with your choice of < vs <= because it looks kind of random.
  • (stylistic) Consider adhering to proper python naming conventions for your variables. Always use snake_case for (most) variables and functions. So, I’d recommend to put thomas instead of Thomas. See PEP 8 for details and more common conventions. Also, I feel like Thomas_task isn’t a very good, descriptive name for the function.
  • (stylistic) The parentheses around the call to input(), in the Δy assignment line, can be removed.
  • You have a local variable Δy in Thomas_task that “shadows” the global Δy variable. I’d recommend changing the global Δy to something else to avoid name conflicts.
  • It is difficult to test your code with assert statements because Thomas_task affects a shared variable, Thomas, so the tests can have side effects. You’d have to refactor your code, such as by returning two values from Thomas_task, to prevent this.

(You can ask me for any clarification or explanation for any of the improvements)

1 Like

In bullet point 10, where you mention the term shadow, what do you mean by that?

“Shadowing” means that you are hiding one variable with another variable, which can lead to subtle bugs.

For example, consider this:

input = input('hello').strip()
name = input('name?').strip()

Notice that the second line gives an error because you assigned a variable that covers up the builtin input.

Similarly, you have a global variable Δy assigned right before Thomas_task is defined. But inside Thomas_task, you also have a variable named the exact same thing. Now there is no way for you to access the global Δy inside of Thomas_task because the local Δy shadows it. This may lead to subtle bugs if you try to extend your code later.

1 Like

Hi, this is what i have so far. I haven’t sorted out the assertion problem but i will work on it later.


import turtle

thomas = turtle.Turtle()
thomas.shape('turtle')
thomas.penup()

screen = turtle.Screen()
screen.setup(1000, 1000)

My_input = input('How high do you want thomas to go Vertically?').strip()

def my_input_check(y):

    thomas.goto(0,0)

    try:
      y = int(y)
    except:
      thomas.fd(-10)
      return '    Please enter a valid integer!'

    if 200 < y or y < -200:
      thomas.fd(-10)
      return '    Hey, thats not a number between 200 and -200!'


    thomas.goto(0, y)

    if 150 < y and  y < 200:
        return '    This is very high!'
    if 100 < y and y < 150:
        return '    This is high!'
    if 0 < y and y < 100:
        return '    This is high but not high enough!'
    if y < 0 and -100 <= y:
        return '    This is low but not low enough!'
    if y < -100  and -150 <= y:
        return '    This is low!'
    if y < -150 and -200 <= y:
        return '    This is very low!'








thomas.write(my_input_check(My_input))

Looks great.

One way to sort out the assertion problem is to have my_input_check never do anything with thomas. So no side effects and no referencing changing globals, which means it will be a “pure” function. You can achieve this by returning the position to use, along with the message. Now you can use assertions with no risk of side effects, and as a bonus you can test position now too.
On a side note, it would be good to replace all calls to thomas.fd(...) with calls to thomas.goto(...) to keep it consistent if you won’t refactor your code.

just a few more improvements:

  • only catch ValueError in your try-except, like you did before
  • for all of the range checks, combine each into chained comparison
  • (stylistic) change My_input to snake_case: my_input
  • it is best practice never to let a function return “implicitly”, meaning it returns at end of function without a return statement. Instead, raise an exception at the end just in case all checks somehow fail.
...

def my_input_check(y):
    try:
        y = int(y)
    except ValueError:
        return (-10, 0), '    Please enter a valid integer!'
    pos = 0, y

    ...

    if -200 < y <= -150:
        return pos, '    This is very low!'
    raise RuntimeError(f'Something went wrong for this value y: {y}')

position, message = my_input_check(my_input)
thomas.goto(position)
thomas.write(message)
1 Like

for the last bullet point, do you mean like raise a exception if the return statement somehow fails?

It means to never let the function go all the way to the end, to catch any errors immediately.
Silly example here, but imagine if the code was a lot more complex and prone to error:

def good():
    if my_condition:
        return foo
    if not my_condition
        return bar
    raise RuntimeError('The code should never reach this point')

So when defining a function, you should address all the errors first before actually writing the ‘real code’?

Sorry if I’m unclear, I mean that if some logic errors make your function skip all of your return cases, then you should raise an error at end, rather then let the function implicitly return None.

def good(should_be_true):
    if should_be_true:
        return 'yay'
    raise RuntimeError('not good')

def bad(should_be_true):
    if should_be_true:
        return 'yay'

# fine \/
yay1 = good(True)
yay2 = bad(True)

# logic error inputs \/
val1 = good(False)  # raises RuntimeError so you know what went wrong
val2 = bad(False)  # val2 is None, but you might not realize this
# val2 will lead to confusing bugs

Counldn’t you use assert statements to check to see if all your conditions in the function are true?

Is this beneficial for when you have blocks and blocks of code and one of them either raise an error or does not work. Then you know which one to change?

Does the exception raised have to be a runtime error?

So, here’s a more practical example, related to what you are building. Consider this function:

def get_message(inp):
    try:
        inp = int(inp)
    except ValueError:
        return 'Bad input'
    # btw, for non-chained comparisons, it's fine to have variable be first in both comparisons.
    if inp > 200 or inp < -200:
        return 'That is not within 200 and -200!'
    if 0 < inp < 200:
        return "That's high"
    if -200 < inp < 0:
        return "That's low"
    # my code should never get to this comment, but it sometimes does
print(get_message(input()))

Does this function always return a string message?
Actually, no, it sometimes returns None! An input of '200' or '0' will make the function return None, which is hard to spot (go through carefully to see why).

You can’t really use assert statements at the start of the function here because you don’t know what logic error you’d be making, you’d not know what to test.

Imagine if you were trying out your program and then your turtle kept on writing “None” and you didn’t know why. That’s what the current function might lead to.

The fix here is to raise an error when the code gets to where it shouldn’t.

def get_message(inp):
    try:
        inp = int(inp)
    except ValueError:
        return 'Bad input'
    # btw, for non-chained comparisons, it's fine to have variable be first in both comparisons.
    if inp > 200 or inp < -200:
        return 'That is not within 200 and -200!'
    if 0 < inp < 200:
        return "That's high"
    if -200 < inp < 0:
        return "That's low"
    # Raise an error here \/
    raise RuntimeError(f'something went wrong for input: {inp}')
print(get_message(input()))

This is much better because instead of getting confusing bugs like the turtle writing “None”, you get a nice error message right at the call to the function, and it even tells you what input caused the error.

The error doesn’t have to be a RuntimeError, but this seems the best fitting exception type for this case. See Built-in Exceptions — Python 3.12.0 documentation for more information on exceptions (errors).


I’m not sure what you mean. Raising an error here helps catch when at least one block (if condition) is not working properly inside the function that allows the function to make it all the way to the end (when it shouldn’t).

I was trying to say that if you have multiple functions performing different tasks but you don’t know which one is not performing correctly, you can identify which function isn’t performing right through the error raised by that function

Yeah, that’s exactly the purpose of raising an error here.
People often say it is better for a program to crash early than to continue running with errors.

1 Like

hi, I keep getting an assertion error when i run the program with the assertion tests. Do you know what i did wrong or could be the issue?


import turtle

thomas = turtle.Turtle()
thomas.shape('turtle')
thomas.penup()

screen = turtle.Screen()
screen.setup(1000, 1000)

my_input = input('How high do you want thomas to go Vertically?').strip()

def my_input_check(y):
  
  try:
    y = int(y)
  except ValueError:
    return (0, -10),'    Please enter a valid integer!'

  if  y > 200 or y < -200:
    return (0, -10),'    Hey, thats not a number between 200 and -200!'

  pos = 0,y

  if 150 < y <= 200:
      return pos,'    This is very high!'
  if 100 < y <= 150:
      return pos,'    This is high!'
  if 0 < y <= 100:
      return pos,'    This is high but not high enough!'
  if -100 <= y < 0:
      return pos,'    This is low but not low enough!'
  if -150 <= y < -100:
      return pos,'    This is low!'
  if -200 <= y < -150:
      return pos,'    This is very low!'
  raise RuntimeError(F'Something went wrong with the value entered for y:{y}')



assert my_input_check("uolkhdaw") == (0, -10),'    Please enter a valid integer!'
assert my_input_check("300") == (0, -10),'    Hey, thats not a number between 200 and -200!'
assert my_input_check("20") == (0,20),'    This is high but not high enough!'
assert my_input_check("140") == (0,140),"    This is high!"
assert my_input_check("160") == (0,160),"    This is very high!"
assert my_input_check("-90") == (0,-90),"    This is low but not low enough!"
assert my_input_check("-140") == (0,-140),"    This is low!"
assert my_input_check("-180") == (0,-180),"    This is very low!"
assert my_input_check("-sadw") == (0, -10),"    Please enter a valid integer!"

postion, action = my_input_check(my_input)
thomas.goto(postion)
thomas.write(action)