3.3.1. Tools and techniques for catching coding issues

3.3.1.1. Initial setup for the Lab

  1. In the VSCode terminal enter:

    cd /workspaces/`ls /workspaces` && mkdir -p lab-d
    cd /workspaces/`ls /workspaces`/lab-d
    uv init .
    

    Remember to enter these one at a time, not both together.

    This will make a new Python project in the current folder. It will be named automatically after the folder name.

    To analyze these lines:

    • cd /workspaces/`ls /workspaces` && mkdir -p lab-d makes the lab-d folder. There are no pre-downloaded files needed for Lab D. Sometimes this means that the Lab D folder isn’t downloaded automatically (because it’s empty).

    • cd /workspaces/`ls /workspaces`/lab-d makes sure we are working in the lab-d folder.

    • uv init . is the interesting command. This actually sets up our virtual environment.

    The above will make the a pyproject.toml file, a main.py file, and a number of others.

  2. We’re now going to add a bit more structure to our code folder, as we discussed in the theory part for Week 3. We’re just going to:

    1. Make a folder called src, and put our code files (just main.py at the moment) in there.

    2. Make a folder called tests. At the moment this will be empty, but we’ll put some unit tests in there in the second part of the lab.

    3. Make a folder called docs. This is where the documentation for the code will live. We’re going to keep this empty, as we won’t focus on writing documentation in this lab. Nevertheless, we’re going to make it to help remind you that for actual projects there should be some documentation files to accompany your code.

    4. Make an empty __init__.py file in the tests folder. This is needed for the unit testing setup later on.

    5. Run the boilerplate main.py file so that a virtual environment gets made.

    Make sure that your terminal is in your Lab D folder, and then enter:

    mkdir src tests docs
    mv main.py src
    uv run src/main.py
    touch tests/__init__.py
    

    When done, your VSCode should look like the below.

    Changing the Python interpreter

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

    Once you’ve opened a Python file, make sure that VSCode says it’s using the lab-d virtual environment. This will be shown in the bottom right hand corner of the screen. If it’s picked a different virtual environment, you can click on the environment name and select the correct one, as shown below.

    Changing the Python interpreter

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

    Selecting the wanted interpreter

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

3.3.1.2. Static code analysis

You’ve probably already seen the static code analysis at work. It underlines pieces of your code, like a spell checker does, to help spot issues before you run the code. In our devcontainer we automatically installed the Ruff analyser for you. We’ll just look at static code analysis very briefly here.

  1. Replace the code in your main.py file with the code below.

    def main()
        print(Hello from lab-d!")
        x == 9
        y = 5
        if x > y:
            print(f"x is greater than {y}")
        else if x == y
            print(f"x is {y}")
        elsif x < y:
            print(f"x is less than {y}")
        else:
            raise ValueError("Unexpected comparison result")
    
    
    if __name__ == "__main__":
        main()
    

    This code has quite a few mistakes in it! If you hover over the red underlined sections, VSCode will display help, to try and help you identify the issue(s) with the code.

    Fix this code.

3.3.1.3. The debugger

The debugger lets the program pause during its execution. We can then examine the state of the program, the variables and similar, to see whether they are what we would expect. We can use this to help track down issues with the code. In the second part of Lab C we used the Jupyter variable explorer to similarly look at variables, but the debugger is more versatile and flexible for when you move on to more complicated programs.

3.3.1.3.1. General debugging

To use the debugger, you add a breakpoint. Execution of the program proceeds as usual, until it reaches a breakpoint. It then stops, and shows a debugging view. You can have as many breakpoints as you would like in different places in your code, we’ll just use one below.

  1. Copy the solution given above into your code file. (We suggest you use our given solution rather than your own code because we’ll refer to some line number below, which might be different in your code.)

  2. Add a breakpoint on Line 5 of the code (assuming you’ve copied the solution given above). To do this, click next to the line number. A red circle should appear, indicating that the code will stop running when it gets to this point.

  3. Ask VSCode to debug the Python file. Click the down arrow next to the Run button, and then select Python Debugger: Debug Python File.

    Setting a breakpoint and starting the debugger

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

  4. You’ll be presented with a view, highlighting Line 5. Line 5 hasn’t been run yet, all of the code up to this point has been run. Remember, the code doesn’t necessarily run from top-to-bottom. Here the code starts with the if __name__ == "__main__": block, which then called the function main().

    The values of x and y are shown. You can compare these against what would you expect them to be.

    The VSCode debugger

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

  5. You’ll also see a set of controls presented. Hover the mouse over these to see what each button does. Briefly:

    • Continue resumes execution of the program. It will run until it reaches another breakpoint, or until it reaches the end of the program, whichever is sooner.

    • Step over advances the execution by 1 line. This is really helpful for stepping through the code line by line to find issues.

    • Step into advances the execution, and if there’s a function, moves the debugger into the function. Otherwise, Step over just runs the function and puts the results into the debugger without looking inside the function.

    There are also buttons to Restart the debugger and to Stop.

    Spend some time exploring these different options. Stop the debugger when you’re ready.

3.3.1.3.2. Changing the value of a variable

  1. The debugger is an interactive tool, to help us understand our programs (and why they might not be working). Right click on the x variable that’s displayed in the debugger. Click on Set Value, and you’ll be able to change the value of x.

    Changing values in the VSCode debugger

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

  2. Set the value of x to 5 and then press the Step Over button. You should see that the if statement now goes down a different route. This can be very useful for checking different options, without having to re-run all of the code.

    Changing values in the VSCode debugger

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

3.3.1.3.3. Examining the call stack

  1. Stop the debugger and start it again (so that we have the right value for x).

  2. We put our breakpoint on Line 5. This is in a function called main(), which is called on Line 18 of the code. The debugger lets us view the status of the program when it was stopped (Line 5), and the status of when main() was called (Line 18).

  3. Click on <module> in the CALL STACK area. This lets us switch the view of the debugger to where the function was called. If you have a breakpoint in a function, which was called by another function, which was called by another function, there might be several layers to this stack. Here we just have one, with the top layer called <module> by default.

    Changing the scope of the variables shown

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

    The debugger now shows which line we’re paused at (in yellow) and where the containing function was called from (in green). As we’re looking at <module>, the variables shown are the ones that it sees. That is, the value of the variables before we started main(). You can see that x = 3.54 in the debugger. Inside main() x = 9. Both variables have the same name, but as they are in different scopes they don’t conflict with one another. The debugger can be moved between different scopes to help with our debugging.

3.3.1.3.4. Conditional breakpoints

We can also have breakpoints that only trigger in certain conditions. This is very useful for tracking down bugs that only occur in some instances.

  1. Replace the code in your file with that given below.

    def countdown(x):
        countdown = range(x,-1,-1)
        for i in countdown:
            print(f"{i}")
        print("Blast off")
    
    
    def main():
        print("Hello from lab-d!")
        x = 9
        y = 5
        if x > y:
            print(f"x is greater than {y}")
        elif x == y:
            print(f"x is {y}")
        elif x < y:
            print(f"x is less than {y}")
        else:
           raise ValueError("Unexpected comparison result")
    
        countdown(x)
    
    
    if __name__ == "__main__":
        a = 56  # used in the next part of the lab
        x = 3.54  # used in the next part of the lab
        main()
    

    This calls a function countdown() once the if statement has completed.

  2. Put a breakpoint on the line print(f"{i}"). Then right click on the red circle, and you’ll be given the option to Edit Breakpoint...

    Setting a conditional breakpoint

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

  3. You can then set an expression that determines whether the breakpoint triggers. You use the same syntax as in an if statement. Type in i == 1 and then press Enter on the keyboard. (Make sure you understand why we’re triggering on i and not on x.) Here we’re using a simple conditional statement, but you can build up more complicated ones if you would like.

    Setting a conditional breakpoint

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

  4. You should see that the debugger has paused the program when i = 1. All of the for loop iterations before this have been run. There are now also 3 functions on the call stack.

    The debugger paused after a conditional breakpoint

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

There’s lots that can be done with the debugger, we’ve just shown a few key features here. Spend some time exploring different options before moving on. We won’t refer back to the debugger again so much in the rest of course, but assume you’ll make use of it to help fix issues in your code and to help check that your code is working as expected.

3.3.1.4. Logging

The debugger is a very powerful tool when we want to interactively work with our code - typically when we want to see the values of particular items and see whether they are what we expect. However, sometimes we just want to run the code, have it do everything, and then have a look at the end at what happened. For example, maybe the code takes a long time to run, and so we’ll leave it running overnight rather than sitting at our computing and waiting for it to reach a breakpoint.

We can make a log file to store values from our program run for later inspection.

Note

You could just use print commands to display the values of variables at different points, e.g. print(f"{i}"), and even save this display to a file. There’s nothing wrong with this per-se, except that:

  1. Python has built in logging functions to make this process smoother and with more power options without having to do everything yourself.

  2. If you have lots of print commands for debugging, when you’ve finished debugging you need to remember to remove or comment out all of the print commands. Otherwise your code will be creating a load of unnecessary output, and displaying stuff to the screen actually takes a relatively long time which could slow down the program execution. In contrast, logging functions have the ability to be turned on and off via a setting. The code can stay the same regardless of whether you’re debugging it or actually using it, just the setting changes.

As with the debugger, there’s lots of depth that we could go into on logging. We’re just going to touch on a few items, so that you have familarity if you need it, and so you don’t always have to resort to using print statements.

We’re going to use the logging library, which is part of the standard library. This means it is avaiable with all Python installations. Logging is such a common need that there are external libraries, such as loguru which add more functionalty, but we won’t look in to these here.

  1. Make a new Python file, with any suitable name, stored in your src folder for Lab D. Put the code below into it. (Note that you can’t call your file logging.py because this will conflict with the name of the logging library we’re importing.)

    import logging
    
    logger = logging.getLogger(__name__)
    
    
    def countdown(x):
        countdown = range(x, -1, -1)
        for i in countdown:
            print(f"{i}")
        print("Blast off")
    
    
    def check_x():
        logger.info("Hello from lab-d!")
        x = 9
        y = 5
        if x > y:
            logger.info(f"x is greater than {y}")
        elif x == y:
            logger.info(f"x is {y}")
        elif x < y:
            logger.info(f"x is less than {y}")
        else:
            raise ValueError("Unexpected comparison result")
    
        countdown(x)
    
    
    def main():
        # Set up logging
        log_filename = "log.txt"
        logging.basicConfig(filename=log_filename, level=logging.INFO)
        logger.info("Started")
    
        # Run wanted functions
        a = 56  # used in the next part of the lab
        x = 3.54  # used in the next part of the lab
        check_x()
    
        # Tidy up logging
        logger.info("Finished")
    
    
    if __name__ == "__main__":
        main()
    

    This code is fundamentally the same as we’ve been developing so far in this lab, but the functions are now in check_x() rather than main(). We’ve also added logging commands. For the logging:

    • import logging and logger = logging.getLogger(__name__) at the top of the code set up the logging library for use.

    • log_filename = "log.txt" and logging.basicConfig(filename=log_filename, level=logging.INFO) in main() configure the logging. Here we’ve asked it to store the log in a file called log.txt. We’ve also set the logging level to INFO. We’ll discuss this in a minute.

    • logger.info() sets what we record into the log file. We’ve added some start and stop messages, and otherwise replaced various print commands with logger.info() commands.

  2. Run this code. You will find that it creates a file called log.txt in your Lab D folder. If it doesn’t show up straight away, press the Refresh button in the VSCode file explorer.

    A log file created by the logging library

    Screenshot of VSCode, software from Microsoft. See course copyright statement.

    Rather than being displayed to the screen, our messages are now being logged in a file for us to inspect later on.

    If you run the code again, you’ll find that the second run is appended to the end of the log file.

  3. Delete the file log.txt to start afresh.

    Change the code line logging.basicConfig(filename=log_filename, level=logging.INFO) to logging.basicConfig(filename=log_filename, level=logging.WARNING), and run the code again.

    The log file is now blank. This is because we’ve now asked for only WARNING level messages to be recorded. Our current code only has INFO level messages, and so nothing is recorded. We can thus get control over how detailed or otherwise we want our logging to be, without having to make lots of changes to the code.

    The logging library has different levels of logging. From highest to lowest these are:

    • CRITICAL

    • ERROR

    • WARNING

    • INFO

    • DEBUG

    When setting the logging level, only messages at that level and above are recorded.

  4. In the code you copied for this part of the lab there is a function countdown(). Change this so that rather than using print statements the countdown is logged at level WARNING, and the blast off is logged at level CRITICAL. Run the code with the logging level set to WARNING. You should see that only the countdown numbers are recorded in the log file.

    There are lots of ways in which you can customize the log file, but we won’t look into these here. The important point is that, generally, print statements should be reserved for when you want to display information to the user. For debugging, logging gives more flexibility and control.

  5. Check your files into Git before moving on.