3. Basic Programming (Control Structures)

In this chapter and the next, we will continue to extend hello_pygame.py and go over some Python basics.

If you are already familiar with Python, a lot of this is just a refresher, so you may want to skip the grammar explanations and start with the Practices (i.e., applications to hello_pygame) and Exercises, and go back to the explanations if you don’t understand them.

On the other hand, if the grammar explanation here is too brief, you may want to refer to the mini-tutorial in Appendix A. If this is still too difficult for you, you may want to read a Python primer book for beginners in programming (not for Python beginners).

3.1. Basic Calculations

3.1.1. Execution of Statements

  • Basically, one line is one sentence. Do not break a line unless specifically allowed.
  • Indentation also has a meaning, so do not insert arbitrary spaces at the beginning of a line
  • Portion of line from # to the end of the line is a comment. It does not affect the execution result.
  • If you put at the end of a line, the line is continued by the next line.
  • You may freely break lines within brackets such as () [] and {}.
How many characters should a line be?
In our pre-configured development environment, if a line is longer than 100 characters, it will be marked as “Line too long” (blue underline).

Traditionally, it is often said that a line should have 80 characters or less, and many people believe that this should be the case for Python as well. However, there is no strong evidence for this number, since 80 comes from the maximum number of characters that a computer screen was able to display in ancient times, or even further back, the number of digits on a punch card. In modern times, 80 characters seems a bit too small.

However, there is a limit to the width of the human field of vision and the ability to move the eyes, so too long is not good.

100 characters is the default setting of pylint, which performs grammar and style checking in VSCode. In our environment, this setting is left unchanged.

In the editor area, vertical lines are displayed at 80 characters and 100 characters. Please use them as guides.

3.1.2. Variables and Operations

As in many programming languages, mathematical expressions can be expressed using arithmetic operators and round parentheses:

(2 + 3) * 4  # (2 + 3) × 4 = 20
10 / 4       # 2.5
10 // 4      # 2 (integer division)
10 % 3       # remainder
10 ** 3      # 10 to the power of 3

An assignment statement can be used to assign a right-hand side value to a left-hand side variable:

pi = 3.14
radius = 5
area = pi * radius ** 2

You can also use the cumulative assignment operators as in C:

x += 2
x *= 5

Parallel assignment is a unique feature of Python:

x, y = (100, 200)
x, y = y, x

3.1.3. Types and Type Conversion

Each value has a type to which it belongs:

5           # value of type int (integer)
5.0, 2e-3   # value of type float (real number)
True, False # balue of type boolean (truth value, logical value)

By using a type name as a function, you can convert it to that type:

float(5)    # 5.0
int(2.5)    # 2
bool(1)     # True (Non-zero values are converted to True)
Does Python have types? I thought there were no types in Python, since there is no need to write type names like int x = 0; as in C.
All data in Python have types. You just don’t need to specify the type of the variable.

First of all, try to distinguish between variables and the data (values) assigned to them. x = 30 in Python assigns the variable x the integer-type value of 30. 30 has the type integer, but x has no such specification. Immediately after that, x = "abc" can be used to assign a string-type value.

In the C language, the types of all variables (i.e., which type of data are allowed to be assigned) are determined before the program execution starts. This is called static typing. In contrast, when types are determined at runtime, as in Python, it is called dynamic typing.

Surface we used in previous chapter is also a type, isn’t it? Then, can it be converted to int or float?
Type conversions can only be done between types for which the conversion is defined. The conversion from Surface to int or float is not defined.

3.1.4. Practice

Variables are of course useful when you are using the same value over and over again, but even if you are only using it once, they can help make your code more readable.

For example, in hello_pygame.py, we created a window with screen = pygame.display.set_mode((600, 400)), but when we look at it later, we may wonder, “What is this 600? What is 400?” It is easier to read if you introduce variables and give them proper names. (This is called an “explanatory variable”.)

Refactoring is the process of modifying a program to make it more readable and maintainable, without changing its functionality, as in this example.

hello_pygame.py (ver 8.0)
 1import pygame
 2
 3pygame.init()
 4width, height = 600, 400
 5screen = pygame.display.set_mode((width, height))
 6font_size = 50
 7font_file = None
 8antialias = True
 9font = pygame.font.Font(font_file, font_size)    
10text_image = font.render("hello, pygame", antialias, pygame.Color("green"))
11player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
12
13while True:
14    pygame.event.clear()
15    key_pressed = pygame.key.get_pressed()
16    if key_pressed[pygame.K_ESCAPE]:
17        break
18    mouse_pos = pygame.mouse.get_pos()
19
20    screen.fill(pygame.Color("black"))
21    screen.blit(player_image, mouse_pos)
22    mouse_x, mouse_y = mouse_pos
23    text_offset_x = 100
24    screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
25    pygame.display.update()
26
27pygame.quit()
Are there any rules on how to name variables?
The grammatical rule is that a variable name must begin with an alphabet or underscore, followed by zero or more alphanumeric characters or underscores. In addition, there are conventions that should be followed.

Names that programmers introduce arbitrarily, such as the names of variables and functions, are called identifiers (whereas predefined names, such as if and while, are called keywords or reserved words).

Identifiers can consist of uppercase and lowercase alphabets, underscore _, and numbers, but remember that the first letter must not be a number.

Identifiers that begin with two underscores are defined for special purposes, and should not be defined by programmers. (e.g. __init__, __name__, etc.)

As is customary in the Python industry, variable and function names should be written in lowercase letters where words are separated by underscores, like snake_case. However, it is recommended that constants be written in uppercase, as in UPPER_CASE.

Why don’t you capitalize width, height, and font_size, which I think are constants?
These may be against Python convention, but there is a reason why they are not capitalized.

In this version of the program, they are indeed constants. However, as we develop the program, they may become non-constant. For example, in the future, you may want to add the ability to change the window size or font size dynamically. And by that time, you may be using these variable names here and there in your programs.

If you capitalize a variable name just because it looks like a “constant” in your current program, you will have to go around converting all occurrences to lowercase when you need to make such a change. This is utter nonsense.

For these reasons, it is the author’s personal opinion that the Python convention of capitalizing constant names should only be applied when it is certain that the constant will remain a constant in the future.

3.2. Functions

3.2.1. Function Definition and Function Call

Function definitions are written in the following format:

def function_name(argument1, argument2, argument3):
    The Function Body
    ...
    return return_value
  • If there are no arguments, the parentheses should be empty.
  • The return statement should be written only as return if there is no return value.
  • If the function is exited without reaching the return statement, it is the same as executing a return statement without a return value.
  • Variables defined in a function become local variables that can be used only within that function.

A function is called by placing a pair of round parentheses after the function name:

function_name(argument1, argument2, argument3)
A function is something like \(z = f(x, y)\) in mathematics, where x and y are the arguments, and z is the return value, right? If so, I don’t understand why some functions don’t take any arguments or don’t return any values.
The concept of functions in Python, C, and other programming languages are not the same as functions in mathematics.

In mathematics, a function determines a unique return value once the arguments are specified. In addition, there are no side effects other than returning a value (for example, the value of an unrelated variable is changed, or the contents of a file are rewritten). In the field of programming languages, such functions are called “pure functions”.

Python and C functions are just a series of processing that can be named and called. They can be used to represent pure functions, but they can also be used for other purposes. For example, suppose there is a function that takes a file name as an argument, reads the number written in the file, and returns it as a value. This function is clearly not a pure function, because the return value depends on the contents of the file, even if the same argument is passed to the function.

Aside from whether it is pure or not, it is understandable that you feel resistance to calling something that does not return a value a function. In fact, some programming languages, especially older ones, have a different syntax for non-value-returning functions, calling them procedures, subroutines, or subprograms.

When defining a function, you can specify a default value for an argument:

def func_abc(a, b=20, c=10):
    return (a + b) * c

y = func_abc(2, 3, 5)   # (2 + 3) * 5 = 25
z = func_abc(2, 3)      # (2 + 3) * 10 = 50

When calling a function, you can sometimes designate arguments by keywords:

y = func_abc(2, b=3, c=10)  # (2 + 3) * 10 = 50
z = func_abc(a=2, c=3)      # (2 + 20) * 3 = 66

A good combination of these will make it easier to specify arguments when calling a function.

Keyword arguments and default arguments seem to be allowed in some cases, and not allowed in other cases. I’m quite confused.
It’ll be difficult to understand the detailed rules completely, so it’s better to just understand the basic principles and get used to them.

First of all, when defining a function, arguments with default values need to be placed after those without. This is obvious because you can’t place optional arguments before mandatory ones.

Next, when calling a function, the keyword arguments must be placed after the non-keyword arguments. This is because keyword arguments can be specified in any order, and it is confusing if there are ordinary positional arguments after them.

In addition to the above basic points, there are other ways to add restrictions to function definitions, such as “these are arguments that can only be specified by keywords” or “these are arguments that cannot be specified by keywords”. You may or may not be able to tell from the manual whether a given function has this restriction. Unfortunately, the pygame reference manual is of the type from which you cannot tell. So you need to try out and then figure out “Oh, this function does not allow keyword arguments…”. It can’t be helped.

3.2.2. Practice

In hello_pygame.py so far, we have written all the operations in the file level without separating them into functions, but now that the size of the program is getting larger, we will separate them.

When the same process is performed in multiple places in a program, it should be combined into a function of course. But this is not the only case where functions are useful.

First of all, naming a sequence of operations, even if it is only executed once, makes it clear what it is trying to do.

Secondly, variables defined in a function become local variables that can only be used within that function. By localizing the effects of variables, the behavior of the program becomes easier to understand, resulting in a program that is easier to read and less prone to errors.

In contrast, in our current program, all variables are global variables that can be seen by the entire program, and it is necessary to always be aware of which part of the program a variable is used in and where in the program it can be changed.

In the following chapters of this textbook, I will introduce some ideas for managing programs that tend to grow in size and complexity, but they all have one thing in common: reducing the number of factors you need to keep track of at the same time. The avoidance of global variables is the most basic practice of this rule.

The first step in dividing the code into functions is to move all the code at the file level into a single function. You can name the function whatever you like, but let’s call it main (following the C language).

hello_pygame.py (ver 9.0)
 1import pygame
 2
 3
 4def main():
 5    pygame.init()
 6    width, height = 600, 400
 7    screen = pygame.display.set_mode((width, height))
 8    font_size = 50
 9    font_file = None
10    antialias = True
11    font = pygame.font.Font(font_file, font_size)
12    text_image = font.render("hello, pygame", antialias, pygame.Color("green"))
13    player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
14
15    while True:
16        pygame.event.clear()
17        key_pressed = pygame.key.get_pressed()
18        if key_pressed[pygame.K_ESCAPE]:
19            break
20        mouse_pos = pygame.mouse.get_pos()
21
22        screen.fill(pygame.Color("black"))
23        screen.blit(player_image, mouse_pos)
24        mouse_x, mouse_y = mouse_pos
25        text_offset_x = 100
26        screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
27        pygame.display.update()
28
29    pygame.quit()
30
31
32main()
Line 1
The import statement is usually not included in the function, so it is left as is.
Line 4
def main(): starts the function definition. There are no arguments, so leave the parentheses empty.
Lines 5-29

You can create these lines by transferring the code from the previous version. However, you need to indent the beginning of the code, not just paste it.

It’s not easy to do this by pressing the Tab key one in a line-by-line manner, so let’s learn a shortcut key to change the indent at once. Drag the target code section to select it, and then press the following keys.

  • Ctrl + ]: Add one level of indentation at once
  • Ctrl + [: remove one level of indentation at once

Since there is no need to return a value, the return statement is omitted; you may put a line at the end that just says return.

Line 32
The main function is called. Even if there are no arguments, an empty () is needed.

Then, let’s refactor the drawing process in lines 22-27 into a function called draw. How about adding:

def draw():
    screen.fill(pygame.Color("black"))
    screen.blit(player_image, mouse_pos)
    mouse_x, mouse_y = mouse_pos
    text_offset_x = 100
    screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
    pygame.display.update()

just as we did for the main function, and write draw() in the original place to call it…? Unfortunately, it won’t work. If you try it, you will get an error.

This is because text_image, player_image, etc. are local variables that can only be used in the main function. They cannot be used in the draw function.

Therefore, we need to pass the necessary variables as arguments.

hello_pygame.py (ver 10.0)
 1import pygame
 2
 3
 4def draw(screen, player_image, text_image, mouse_pos):
 5    screen.fill(pygame.Color("black"))
 6    screen.blit(player_image, mouse_pos)
 7    mouse_x, mouse_y = mouse_pos
 8    text_offset_x = 100
 9    screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
10    pygame.display.update()
11
12
13def main():
14    pygame.init()
15    width, height = 600, 400
16    screen = pygame.display.set_mode((width, height))
17    font_size = 50
18    font_file = None
19    antialias = True
20    font = pygame.font.Font(font_file, font_size)
21    text_image = font.render("hello, pygame", antialias, pygame.Color("green"))
22    player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
23
24    while True:
25        pygame.event.clear()
26        key_pressed = pygame.key.get_pressed()
27        if key_pressed[pygame.K_ESCAPE]:
28            break
29        mouse_pos = pygame.mouse.get_pos()
30        draw(screen, player_image, text_image, mouse_pos)
31
32    pygame.quit()
33
34
35main()
It’s kind of a pain. I’ve programmed with global variables in other languages before, without any trouble. I think it’s more complicated to think about arguments and return values.
That may not have been a problem in small programs, but as programs become larger, global variables tend to become a hotbed of bugs.

Again, by avoiding global variables, you can program a function focusing on only inside the function, reducing the number of factors you have to consider at the same time. The process of introducing arguments and return values may seem tedious, but once you do it, it will help you in the long run.

Even when the processing is separated into functions, it is still possible to define variables outside of the functions, and these variables become global variables. In this course, we will avoid global variables as much as possible in order to learn how to prepare for large-scale programming.

If global variables are absolutely necessary, let’s limit their use to read-only. The reason is that if the value is not changed during execution, the confusion is expected to be minimal.


In the same vein, let’s refactor the code to initialize pygame and create the window screen and the code to create the text image to functions init_screen and create_text, respectively.

hello_pygame.py (ver 11.0)
 1import pygame
 2
 3
 4def init_screen():
 5    pygame.init()
 6    width, height = 600, 400
 7    screen = pygame.display.set_mode((width, height))
 8    return screen
 9
10
11def create_text():
12    font_size = 50
13    font_file = None
14    antialias = True
15    font = pygame.font.Font(font_file, font_size)
16    text_image = font.render("hello, pygame", antialias, pygame.Color("green"))
17    return text_image
18
19
20def draw(screen, player_image, text_image, mouse_pos):
21    screen.fill(pygame.Color("black"))
22    screen.blit(player_image, mouse_pos)
23    mouse_x, mouse_y = mouse_pos
24    text_offset_x = 100
25    screen.blit(text_image, (mouse_x + text_offset_x, mouse_y))
26    pygame.display.update()
27
28
29def main():
30    screen = init_screen()
31    text_image = create_text()
32    player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
33
34    while True:
35        pygame.event.clear()
36        key_pressed = pygame.key.get_pressed()
37        if key_pressed[pygame.K_ESCAPE]:
38            break
39        mouse_pos = pygame.mouse.get_pos()
40        draw(screen, player_image, text_image, mouse_pos)
41
42    pygame.quit()
43
44
45main()

The functions init_screen and create_text have no arguments. On the other hand, a return statement is added at the end of each function definition (lines 8 and 17) to return the result (both are Surface type image data).

3.3. Flow Control

3.3.1. Conditional Statement

The if statement is of the following form (elif and else may be omitted as appropriate):

if cond1:
    print("cond1 is True")
elif cond2:
    print("cond1 is False; cond2 is True")
elif cond3:
    print("cond1 and cond2 are False; cond3 is True")
else:
    print("cond1, cond2, and cond3 are all False")

For cond1, cond2, and other conditions, specify values of type boolean:

a == 10
a != 10
(not 2 > a) or (not a >= 8)

If you are familiar with C, etc., note that you have to write elif instead of else if.

3.3.2. Repetitive Statement

The while and for statements are of the following form:

while x > 0:
    print(x)
    x -= 1

for x in (10, 20, 30):
    print(x + 3)

The for statement may seem strange to those who are used to C. If you execute the above for statement, you will see:

13
23
33

The values in the tuple after for x in are assigned to the variable x in order, and print(x + 3) is executed. The tuple can be replaced by a data structure of other type representing a series of data (collectively called a Sequence in Python).

  • You can use break statements (to break out of a loop) and continue statements (to skip a current iteration of a loop) in a repetitive statement.
  • You cannot create local variables whose scope (the range where the variables can be seen) is a code block such as while or for. Only function-local ones can be defined.

3.3.3. Practice

The previous version of hello_pygame.py ignored the events by calling pygame.event.clear at the beginning of the loop. Therefore, the X (close) button in the upper right corner of the window did not work.

To get information about the event, we use the pygame.event.get function instead. This function does not simply clear the events that have occurred up to that point, but returns them as a sequence in the order in which they occurred. Since it is a sequence, it can be processed one at a time with a for statement.

The elements of the sequence returned by pygame.event.get are pygame-defined objects of type Event (more precisely, of type pygame.Event). You can find more details in the reference manual, but it’s easier to see a concrete example:

hello_pygame.py (ver 12.0)
29def main():
30    screen = init_screen()
31    text_image = create_text()
32    player_image = pygame.image.load("../../assets/player/p1_walk01.png").convert()
33
34    while True:
35        should_quit = False
36        for event in pygame.event.get():
37            if event.type == pygame.QUIT:
38                should_quit = True
39            elif event.type == pygame.KEYDOWN:
40                if event.key == pygame.K_ESCAPE:
41                    should_quit = True
42        if should_quit:
43            break
44        mouse_pos = pygame.mouse.get_pos()
45        draw(screen, player_image, text_image, mouse_pos)
46
47    pygame.quit()
48
49
50main()

In this example, the program detects the “X” button press event and the Esc key press event. The program is terminated in both cases. In the previous hello_pygame.py, the key presses have been detected by the pygame.key.get_pressed function, but using this style of event processing is more reliable.

Line 35
A should_quit variable to indicate whether or not to quit is defined. If the event we are interested in does not come, we do not quit, so we initially set it to False.
Line 36
One event is extracted from the sequence of events into the variable event.
Lines 37-38

An object of type Event has a variable called type (just like a Surface-type object has methods called fill and blit).

In Python, a variable that belongs to an object is called an attribute. If the type attribute of the retrieved event is equal to pygame.QUIT, a constant provided by pygame, it means that the “X” button in the window title bar was pressed. In that case, should_quit is set to True.

Lines 39-41

If type attribute of the event is pygame.KEYDOWN, it means that some key was pressed. In this case, the key name can be found by examining the key attribute of the Event type object. If it is equal to the pygame.K_ESCAPE constant, then should_quit is set to True.

There is no else clause in the if statement starting on line 37, so events other than QUIT and KEYDOWN will simply be ignored.

Lines 42-43
If should_quit is True at this point, the break statement will break out of the while loop, and the program will exit by returning from the main function after calling pygame.quit on line 47.
Can’t I just break on the fly without using the should_quit variable?
If you break in the if-elif statement, you will only get out of the for loop, not out of the while loop.
Shouldn’t the mouse position be changed to be retrieved by pygame.event.get?
This is fine, because we don’t need to worry about missing of events.

Let’s take an extreme example: a while loop is executed once per second (actually, it’s much faster). If we want to detect a key press, there could be a case where no key is pressed when pygame.key.get_pressed is called at one time, and no key is pressed when pygame.key.get_pressed is called at the next time (1 second later), but a key is pressed between these two times. We cannot detect this key press. This is what we call missing of an event.

By contrast, for mouse position acquisition, it is enough to know the latest mouse position to update the player’s drawing position at each time, and there is no need to consider what happens during intervals.

If you really want to handle it as an event processing, you can use an Event whose type attribute is MOUSEMOTION. But it will be more difficult to use because the event is not fired when the mouse pointer is not moving or is outside the window.

3.3.4. if __name__ == “__main__”

Here is another example of using the if statement. So far, to call the main function, we have simply written main() at the end of the program. But in Python, it is recommended to write as:

hello_pygame.py (ver 13.0)
47    pygame.quit()
48
49
50if __name__ == "__main__":
51    main()

This is in case this file is not invoked directly by the python command, but is imported from another code.

When invoked directly, the variable __name__ is automatically set to the string __main__. By contrast, if it is imported, it is set to the module name (in this case, hello_pygame). As a result, by introducing an if statement like the one above, the main function is called only when it is invoked directly.

I can’t imagine running the same py file directly from the python command in some cases, and importing it from elsewhere in other cases. Is this really necessary?
It is often the case that you want to execute some of functions defined in a file from the outside for checking their behaviors.

For example, you may want to run the following by interactive execution. If you have made the above modifications to hello_pygame.py, you should see the following output:

>>> import pygame
pygame 2.0.1 (SDL 2.0.14, Python 3.9.5)
Hello from the pygame community. https://www.pygame.org/contribute.html
>>> pygame.init()
(7, 0)
>>> from hello_pygame import create_text
>>> screen = create_text()
>>> screen
<Surface(230x36x32 SW)>

Here, after initializing pygame, we import create_text from hello_pygame.py so that it can be used as a function. This is a common way to check the behavior of a function under development.

If you hadn’t done the above revision, and had just written main() at the end of hello_pygame.py, the main function would be called when the line from hello_pygame import create_text is executed. So the window would open, and you wouldn’t be able to proceed until you click the X button in the title bar or hit Esc key to stop.

3.4. Debugging Tips

3.4.1. Understanding Error Messages

As the code is getting more and more complicated, there may be cases where it does not work as expected.

First of all, if you see a red underline while typing code, there is a high probability that something is wrong. Try to fix it as soon as possible.

If you run the program and encounter an error, read the error message carefully. You can learn how to read error messages in the Q&A section of the previous chapter. The more complex the program, the more lines of messages there are, and the more difficult it looks. For example, if you forgot to add the () at the end of mouse_pos = pygame.mouse.get_pos(), you will get the following message:

Traceback (most recent call last):
  File "c:\cs1\projects\hello_pygame\hello_pygame.py", line 51, in <module>
    main()
  File "c:\cs1\projects\hello_pygame\hello_pygame.py", line 45, in main
    draw(screen, player_image, text_image, mouse_pos)
  File "c:\cs1\projects\hello_pygame\hello_pygame.py", line 22, in draw
    screen.blit(player_image, mouse_pos)
TypeError: invalid destination position for blit

It indicates that an error occurred in screen.blit, which is called from draw, which is called from main. The message is long because it contains information about the call history, but it makes it easier to figure out what happened and when.

The last thing written is the error that occurred: invalid destination position for blit, which means that something is wrong with mouse_pos. It is important to note that the line of screen.blit where this error occurs is not necessarily wrong; it is often necessary to check back to the point where mouse_pos was defined or updated. So, if necessary, go backwards through the call history (looking at the error messages from the bottom up).

I tried to search the web for an error message I got, but I couldn’t find any information.
There are a few tricks to searching the web, which are useful not only for error messages. In particular, you should be familiar with the phrase search.

If you type an error message (e.g. SyntaxError: invalid syntax) into Google, it will search for pages that contain many of the words that make up the message (SyntaxError, invalid, syntax).

If you want to find a page that contains the phrase itself, rather than each word, you can enclose it in double quotes, as in "SyntaxError: invalid syntax". This will help you find the message you are looking for more reliably.

Note that it is not sufficient to blindly search the entire message for the phrase. For example, when you run into:

>>> int("hello, world")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'hello, world'

and carry out the phrase search of the entire last line, it will not yield any matching results. This is because the last portion ‘hello, world’ is part of the specific program you wrote, and is not generalizable. You can find useful information by searching for “ValueError: invalid literal for int() with base 10” without this portion.

I’m afraid of getting an error… I feel like I’m getting scolded.
Errors are not a bad thing. Let the computer find the errors.

What you really need to be afraid of is that something doesn’t work well but there’s no error, and what you should be afraid of even more is that something looks to work fine without an error, but actually gives wrong results. So, when you get an error, you should be glad that you found the mistake.

Advanced programmers assume that they will make mistakes, and program as preventively as possible so that when they do make a mistake, an error will occur. Errors are your tool.

What do you mean by “preventive programming” in your previous answer?
Here are two examples: the first is the use of assert statements, and the second is the use of type hints.

As an example, consider the the mouse_pos = pygame.mouse.get_pos() line again. Think of inserting the following line next to this:

assert len(mouse_pos) == 2

The assert statement is in the form of assert <expression>, and it does nothing if the given <expression> is True, but raises an error if it is False.

len is a function that returns the length of the sequence given as an argument. In this example, it expects mouse_pos to be a sequence of length 2. If it is not a sequence, the len function will raise an error, and even if it is a sequence, the assert statement will raise an error if the length is not 2.

In this way, asserting conditions that should be satisfied in the middle of a program allows you to detect unexpected behavior early on.

Another example is type hinting. Python doesn’t require the type of a variable to be specified. However, it is possible to write type names as hints so that automatic check for type consistency can be executed.

To enable automatic type checking, open the VSCode’s configuration file settings.json, replace the off part below with basic and save it (To open settings.json, open View ‣ Command Palette from the menu bar and select Preferences: Open User Settings (JSON)):

"python.analysis.typeCheckingMode": "off",

Get back to hello_pygame.py and add the following line just before the mouse_pos = ... line:

mouse_pos: tuple[int, int]

Or you can modify the mouse_pos = ... line itself into:

mouse_pos: tuple[int, int] = pygame.mouse.get_pos()

This explicitly tells that the variable mouse_pos expects assignment of data of type tuple consisting of two integers.

The python command does not do anything special when written this way. However, external tools can be used to check the type consistency. A type checking tool called mypy is installed in our pre-configured environment and it runs automatically on VSCode. If you forget to add () after get_pos, you will see a blue underline in the editor area.

Notice that while the check by the assert statement does not run until the program is executed, the type check can be done before execution, and the result can be seen immediately in the editor area. Python’s feature of not having to specify the type of a variable is convenient when writing small programs, but when developing large programs, it is often useful to specify types.

If you wish to continue to enable type checking, leave settings.json as it is. If you feel that it is too much of a bother, change the basic back to off.

3.4.2. Making Use of Debugger

If you can find out the cause of the error immediately after reading the error message, it’s easy. However, if you can’t identify the cause of the error, or if the error doesn’t occur but the behavior is strange, you need to make sure that the relevant variables are set to the expected values.

For example, in the case of the above mouse_pos error, insert the line print("mouse_pos = ", mouse_pos) before the line of pygame.blit, and then run it to see the output, and you should see that the value is wrong:

mouse_pos =  <built-in function get_pos>

It can be seen that mouse_pos was assigned with the get_pos function itself, not the result returned by the get_pos function.

If you have an idea of which variable is problematic, this kind of “print debugging” is useful. But if not, it’s best to use the debugger. In the current example, you can place a breakpoint on the pygame.blit line (left-click on the left side of the line number to put a red circle around it) and run the debugger (Run ‣ Start Debugging), which will pause just before pygame.blit is called, so that you can mouse-hover over the variable in the editor, or list the variables in the Run sidebar, to look for variables with unexpected values.

If you don’t get an error, you need to look more extensively. In the case of a program like the current one, it is tempting to set a breakpoint somewhere in the while loop of the main function and check the value of each variable every iteration… but the interference with the mouse operation is a problem. You cannot press the VSCode debugger buttons (Continue, Step Over, etc.) when you are moving the mouse on the window of the program under development.

In such a case, you can intentionally create a statement that will be executed only under certain conditions, and place a breakpoint there.

hello_pygame.py (ver 14.0)
34    while True:
35        should_quit = False
36        for event in pygame.event.get():
37            if event.type == pygame.QUIT:
38                should_quit = True
39            elif event.type == pygame.KEYDOWN:
40                if event.key == pygame.K_ESCAPE:
41                    should_quit = True
42                elif event.key == pygame.K_b:
43                    pass
44        if should_quit:
45            break

pygame.K_b represents the b key, i.e., the pass statement in line 43 is executed only when b is pressed. The pass statement does nothing, so usually nothing happens. If you want to debug it, you can set a breakpoint here so that it pauses when b is pressed. Then you can check the state of the variable of interest at that point.

When debugging, the pygame window does not appear. It appears when I run it without debugging.
Isn’t the pygame window hidden behind the VSCode?
Why does the pygame window sometimes go into a “no response” state when debugging?
This is because no events are processed while the window is stopped at a breakpoint. This is unavoidable.
Hello again. Freaky Notepad programmer here. Is there any way to use the debugger with Notepad?
You can’t use it within Notepad, but you can use the pdb command from the command prompt. How to use it? Find out for yourself.

3.5. Exercises

Problem 3-1

Set a breakpoint in the pass statement, pause the execution with the b key at some point in the execution. Proceed to the line of screen.blit(player_image, mouse_pos) in the draw function, check the value of mouse_pos just before that line, and confirm that it is apparently the same as the current mouse pointer position.

To go forward one line, we usually press the Step Over button, but this does not enter the function. To enter the function, use Step Into. To exit from the function, use Step Out.

Problem 3-2

You can set a conditional breakpoint. Instead of left-clicking on the left side of the line number, right-click and select Add Conditional Breakpoint. set a breakpoint at the line of draw(screen, player_image, text_image, mouse_pos) that will be triggered when the x-coordinate of the mouse reaches 300 or more, and check the value of each variable at that time.

The first value (x-coordinate value) of the tuple mouse_pos can be expressed as mouse_pos[0]. You can specify a boolean expression predicating that this expression is greater than or equal to 300.

Problem 3-3

(Don’t forget to commit in Git before working on this Problem. We’ll start over in the next chapter at this point just before this Problem.)

Modify a function create_text such that it accepts two arguments, text and color, with default values, and change the function to create the text with the specified text and color. If the arguments are omitted, make it behave as it did in the previous version.

Use this modified function to create text_image2 in addition to text_image, and display text_image2 instead of text_image while the Space key is being pressed.

To specify the Space key, you can use pygame.K_SPACE instead of pygame.K_ESCAPE. A list of all keys can be found in the Reference Manual on the following page: