Showing posts with label debugging. Show all posts
Showing posts with label debugging. Show all posts

Tuesday, 4 December 2018

Historical Database ver5 - combining SQLite, Python and HTML

I have had a go at updating the program for looking at and managing the database of historical figures. Last post I was busy altering the SQLite database to add a new column (imagelink, for images associated with each record) and remove an unwanted column (Role, which has been replaced by a subtable to allow multiple roles per personality).

Now I have been modifying the Python program firstly to accommodate the changes in the data structure, and also to display any selected record as a HTML page. This is for two reasons:
Firstly HTML pages can display images when opened in a browser; Secondly HTML can take web links in the database and turn them into clickable hyperlinks. You can't do either of these just on the IDLE command line.(A third reason that isn't put into practice here is HTML makes tables easier, particularly automating column width - useful when you want to display data in tabular form).

 The whole program is now 415 lines long, so I won't just copy and paste the whole thing. That's one of the things I've noticed since starting out learning Python. When you keep working on a program, it can grow surprisingly big. The trick is to make sure that big does not mean unreadable or unmanageable (functions really help here, as do comments in the code).
Making changes to the data structure can mean lots of changes to Python code, and I've been going through the code and testing it, seeing what the ramifications of changing the columns are for the code. This has affected editing existing records, entering new records, displaying the main table and of course displaying records.

This is the function that has changed the most, the one for displaying individual records.

def displayrecord(name):
    curs.execute("SELECT * FROM historicalfigures WHERE Name = '"+ name +"';")
    maindump = curs.fetchall()
    curs.execute("SELECT * FROM roles WHERE Name = '"+ name +"';")
    rolesdump = curs.fetchall()
    curs.execute("SELECT * FROM weblinks WHERE Name = '"+ name +"';")
    webdump = curs.fetchall()
    mainrecord = maindump[0]
    FileH = open("temp.html", 'w')
    FileH.write("<HTML>")
    FileH.write("<h3>Name: " + name +"</h3> \n")
    FileH.write("<p>Born: " + str(mainrecord[1])+"</p> \n")
    FileH.write("<p>Died: " + str(mainrecord[2])+"</p> \n")
    FileH.write("<p>Nation: " + mainrecord[3]+"</p> \n")
    FileH.write("<p>Image: " + str(mainrecord[4])+"</p> \n")
    FileH.write("<br><img src='"+str(mainrecord[4])+"' width=300 align='right'>")
    rolestring = "<p><B>Roles</B></p> <ul>\n"
    for line in rolesdump:
        rolestring = rolestring + "<li>"+ line[1] +"</li> \n"
    rolestring += "</ul> \n"
    FileH.write(rolestring)
    if webdump: # does webdump contain any lines?
        FileH.write("<p><b>Web addresses for more info</b></p> \n")
        FileH.write("<ul>")
        for line in webdump:
            FileH.write("<li><a href ='"+line[1]+"'>"+line[2] +"</a></li> \n")
        FileH.write("</ul>")
    FileH.write("</HTML>")
    FileH.close()
    webbrowser.open("temp.html")



As you can see it first collects the data from SQLite (using the curs cursor object as usual), then creates a file (temp.html) and file handle (a.k.a. io stream) so Python can write to it, and then writes HTML code to the file modified by what the data in the database is. Finally it uses the webbrowser module (imported along with sqlite3 module earlier in the code) to open the newly created web page in your web browser.

Here is the HTML contents of a typical record:

<HTML><h3>Name: Cardinal Thomas Wolsey</h3>
<p>Born: 1473</p>
<p>Died: 1530</p>
<p>Nation: England</p>
<p>Image: https://collectionimages.npg.org.uk/large/mw06903/Thomas-Wolsey.jpg</p>
<br><img src='https://collectionimages.npg.org.uk/large/mw06903/Thomas-Wolsey.jpg' width=300 align='right'><p><B>Roles</B></p> <ul>
<li>Politician</li>
<li>Cleric</li>
</ul>
</HTML>

Any web designer will tell you this is not exactly attractive, but I believe it serves as proof of concept - we can use Python to get data from a database and create a custom webpage using that data.

Tuesday, 23 October 2018

Failing Gracefully

Failing Gracefully is the idea that if something goes wrong when a program is running, the disruption this causes to the user is minimised.  If possible the program keeps going, and even if it has to close, at least it can be polite and informative to the user as it does so. Ideally an end user should never have to see a raw syntax error on their screen. At the end of my previous post when discussing the historical figure database I said
 If the inputs from the user are valid it does not throw any errors. I have not quite ironed out all the errors caused by bad inputs.
This is one important aspect of failing gracefully - coping with user error. Clearly the program isn't good if it fails and throws a syntax error when the user enters valid inputs. But a good program should cope with invalid inputs as well. In terms of the latest program I have created, most of the inputs are tested to see that they valid - there is a function in the program that is dedicated to making sure that inputs that are expected to be integers are indeed integers:

def checkint(num):
    try:
        int(num)
        check = True
    except:
        print("Not a valid number!")
        check = False
    return check

Simple but it does its job. Note that it uses a try...except... structure, which is a very useful thing when checking for something that might cause syntax errors. If a syntax error would occur in a try...except... structure, it is caught before the program crashes, and the except part takes over. 
Another aspect and set of errors is when the program needs to connect to an external resource, such as a file. This can happen when either opening a text file or when using the SQLite module to connect to a database file, or maybe trying to connect to a website. If the file doesn't exist, or maybe not in the folder the program is using as the path, then generally the program will throw an error and end. Are there alternatives? Could the programmer anticipate this? If the external file is essential for the running of the program then the best case scenario may be that the Python program closes, leaving an error message saying that it could not find the relevant file. Should the user be prompted to enter an alternative file path? Could the program run with a different file? Should this alternative file's name be included in the code or should the user be prompted to input it? As above, a try...except... structure can catch these problems before they cause the program to crash. 
Asking the programmer to anticipate all of these problems could add substantially to the code, but not anticipating them at all is courting even more problems, especially when untrained users are let loose on the program. I admit that I do make assumptions about things working as expected (including user inputs) when creating the programs you have already seen on this blog. For example, the historical figures database assumes that SQLite is already installed, and that the history1.db with its three tables and their respective columns is already set up, ready to at least receive new data. That's simply how things are on my computer. Running the program on a different computer will cause problems because the program does not anticipate these differences between computers.

Friday, 19 October 2018

Documentation Outside of the Code

In professional development this is not just useful, it is obligatory. Where I am with my relatively small programs it is just starting to be useful. Commenting in the code is good and useful, but sometimes it isn't enough. This proved true with the Historical database, especially when going through the testing. A simple text file holding my thoughts and ideas about the database proved sufficient. A sample few lines from that text file:

View - Done; all options for sorting tested, and also tested viewing individuals, both with and without weblinks
Search tested - problem if no results found it still creates table and asks for which record to view. Now corrected with if searchdump: to check if searchdump is empty
New Record tested (Martin Luther)
When updating record, if no number is selected (for which record) it throws an error. Just hitting return should return to main menu. - Now corrected
When adding weblink, confirmation needed for which person is selected before adding weblink - Fixed!
Modify searching births and deaths to allow range of years to be searched - maybe leave to version 5
When trying to update record for a character's roles, I can select a role but when I enter the new role it throws an error.
 Traceback (most recent call last):
  File "C:\Users\pc\Documents\Programming\historical_4.py", line 362, in <module>
    updaterecord()
  File "C:\Users\pc\Documents\Programming\historical_4.py", line 303, in updaterecord
    oldvalue = roledump[colcount-5][0]
IndexError: list index out of range
>>>

>>> conn = sqlite3.connect("C:\sqlite\db\history1.db")
>>> curs = conn.cursor()
>>> curs.execute("SELECT Role FROM roles WHERE Name ='Mickey Mouse';")
<sqlite3.Cursor object at 0x03C32EE0>
>>> roledump = curs.fetchall()
>>> print(str(roledump))
[('Cartoon Character',), ('Entertainer',)]
>>> 
Fixed now.
When Updating a record and user changes the name, the subtables are not updated - i.e. the subtables use the old name and are no longer associated with that record
Fixed by inserting the following code just before the COMMIT

    if colchoice == 0:
        executable_role = "UPDATE roles SET Name = "+ newvalue +" WHERE Name = '" + updatename + "';"
        print(executable_role)
        curs.execute(executable_role)
        executable_link = "UPDATE weblinks SET Name = "+ newvalue +" WHERE Name = '"+ updatename + "'";
        print(executable_link)
        curs.execute(executable_link)

Deleting link - at the moment it relies on there being only a few entries in the table - it prints out all links for all names. This may need to change when more links are added, so that the user selects the historical name first then which link associated with that name to delete.  Probably leave until version 5

I've adjusted the fonts to clarify what's what. When testing, I will encounter either problems or possible improvements. Jotting them down in this text file gives me a to-do list, which I can cross off as I address each problem.
From a professional perspective this is not very neat or well-structured. At the moment it doesn't have to be - it is purely for my own reference, there are no work colleagues or fellow developers who need to understand what's going on. Nonetheless, it is a start of what could be a very sensible habit.

Saturday, 13 October 2018

Testing a program

Once a program reaches a certain size, and maybe has a few different functions and options, making sure it does what you intended becomes a task in itself. When you reach the level of professional software for public release, testing becomes an integral part of the process.
As anyone who has encountered bugs in a program they are using, even the professionals don't always get every bug - often because they are not aware of the bug before the software is released to the public.
Before now my programs have been short enough that the range of inputs is fairly simple.
As previously discussed, the two main groups of bugs are syntax errors, where the Python Interpreter does not like what is written (one bracket in a pair is missing, or there isn't a colon at the end of the first line of a while, for or if structure, or a string isn't enclosed in quotes, so the program thinks it's a variable), and logical errors where the programmer gives the Python interpreter valid instructions but the results are not what the programmer wants.
Syntax errors in the main program are usually picked up quickly - a lot will be found when I first try to run the code and IDLE points out the errors that prevent it from doing so, usually the first one encountered when it tries to run the program, and so multiple runs and debugs will work through these syntax errors.
Some syntax errors are not exactly within Python but with another software package that Python interacts with - for example if I am using Python to interact with SQLite and I tell the Python cursor object to execute a bad SQLite command, SQLite will send an error back to Python and Python in turn will interrupt the program with this error.
Some syntax errors are more subtle, particularly if they are within functions. These may only appear once the program calls on the function. For example there is an SQLite error because one function (here searchtable()) sends a bad SQLite command via the cursor:
========= RESTART: C:\Users\pc\Documents\Programming\historical_4.py =========
Historical Figures Table
========================
Main menu - Please select an option
View whole table ---V
Search table -------S
Enter new record ---E
Delete record ------D
Update record ------U
Quit Program -------Q
Add/Delete Role ----R
Add/Delete Weblink -W
? s
Searching table - please select which column
Name ----(N)
Born ----(B)
Died ----(D)
Nation --(T)
Role ----(R)
? r
What Role? explorer
Traceback (most recent call last):
  File "C:\Users\pc\Documents\Programming\historical_4.py", line 336, in <module>
    searchtable()
  File "C:\Users\pc\Documents\Programming\historical_4.py", line 240, in searchtable
    curs.execute("SELECT * FROM historicalfigures WHERE " + searchterm +";")
sqlite3.Warning: You can only execute one statement at a time.
>>>
This particular one turned out to be an unwanted semicolon, making SQLite think there were 2 commands rather than the one intended.
From my own experience, the best way, perhaps only way, to find these errors is to thoroughly test the program, systematically going through each function and trying different inputs, first valid inputs that the programmer expects from the user, and then (if feeling adventurous), invalid or wrong inputs. Ideally the program should not crash simply because the user makes a mistake.
When doing this I will sometimes generate records that are not intended to be kept - for example adding Mickey Mouse as a historical figure. This is for two reasons - Firstly if I get things wrong (in terms of the program parsing my input incorrectly) then any problematic records can be deleted without regret. Secondly it means I am motivated to use the delete functions, which I use less often than the entry/adding functions.

As an aside, some files and databases may be permanently changed as a result of programs being run, such as this historical database being changed when testing the adding, editing and deleting of records. If the data is important, it may be better to create a test copy of the data and test the program using that, or at least create a backup of both the original data and the original program. Here one of SQLite's features is useful - unlike some SQL databases, SQLite keeps each database in a single file that can be copied and pasted like other files.
I'm not going to go through each error I have discovered in the historical database program - it turns out there were a lot more than I had expected.

Another aspect of testing is seeing things from a user's point of view. While testing this historical database program I have sometimes thought "Wouldn't it be convenient if...." or "Wouldn't it be cool if...". These can give rise to improvements to the program. Those improvements will need testing in due course to ensure new errors have not been introduced.

Tuesday, 2 October 2018

Improvements for the database program

Last post I presented a program run on the Python command line (in my case IDLE) that connected to and interacted with a SQLite database of historical figures.
Although it works, there are various things that could be improved. This is often the case with big programs - the developers never truly finish, they just get to a point where it's good enough to release to the public (or maybe they get to a point where they just can't be bothered any more).
So what sort of improvements would I make?
  1. For the viewtable() function I could add an option to sort by different columns. At the moment it just sorts the names alphabetically, but I can imagine the use of sorting by year of birth or nation.
  2. For the SQLite database I could add a field with a web address for the person's Wikipedia entry. That then brings in the question of the best way for users of the Python program to find and use this web address.
  3. For searchtable() function I need to update the way the results are presented - it currently uses an old version of the code to present the table onscreen, which has been updated for viewtable(). This raises another possibility - creating another function to present tables onscreen that is shared by both viewtable() and searchtable(). After all, one of the big advantages of user-defined functions is that they can be called on by different parts of the program, thus eliminating the need to copy and paste commonly used code from one part of the program to another (which is what happened here).
  4. The searchtable() function no longer needs the diagnostic printing of the LIKE terms. Print statements can be useful for debugging (and they were useful when getting this program to work) but when no longer needed they should at least be hashed out as a comment or preferably deleted altogether.
  5. For the SQLite database I could replace the Role column with a new table, which would allow one person to have as many roles as seems appropriate. This aspect of databases involving multiple tables is worthy of a post on its own - see the next one.
  6. One aspect that can be improved for all functions is validating inputs, both for selecting options in the program and data input to be stored on the database - the most important one being checking that integers are integers. If SQLite receives a string/text input when it is expecting a number, it will throw an error, causing the Python program to crash. 
So the newer script is:
import sqlite3
conn = sqlite3.connect("C:\\sqlite\\db\\history1.db")
curs = conn.cursor()

def displaytable(dump):
    print("Name                  | Born  | Died  | Role                  | Nation")
    print("======================|=======|=======|=======================|=========")
    for line in dump:
        namestr = line[0]
        if len(namestr) >= 23:
            namestr = namestr[0:22]
        rolestr = line[3]
        if len(rolestr) >= 23:
            rolestr = rolestr[0:22]
        nationstr = line[4]
        if len(nationstr) >= 17:
            nationstr = nationstr[0:16]
        gapname = " " * (22 - len(namestr)) + "| "
        gapborn = "  | "
        gapdied = "  | "
        gaprole = " " * (22 - len(rolestr)) + "| "
        print(namestr + gapname + str(line[1]) + gapborn + str(line[2]) + gapdied + rolestr + gaprole + nationstr)

def viewtable():
    print("Viewing Whole Table")
    print("Sort table by: N for name, B for year of birth, D for year of death, R for role, T for nation")
    sortby = input("? ")
    sortby = sortby.lower()
    sortdic = {'n':'Name',
               'b':'YearBirth',
               'd':'YearDeath',
               'r':'Role',
               't':'Nation'}
    if sortby in sortdic.keys():
        sortfield = sortdic[sortby]
    else:
        print('Sorry, option not recognised')
        return
    
    curs.execute("SELECT * FROM historicalfigures ORDER BY "+ sortfield +";")
    fulldump = curs.fetchall()
    displaytable(fulldump)
    

def searchtable():
    print("Searching table - please select which column ")
    print("Name ----(N)")
    print("Born ----(B)")
    print("Died ----(D)")
    print("Role ----(R)")
    print("Nation --(T)")
    fieldchoice = input("? ")
    fieldchoice = fieldchoice.lower()
    if fieldchoice == "n":
        term = input("What name? ")
        searchterm = "Name = '" + term + "'"
    elif fieldchoice == "b":
        compare = input("Do you want results before ( < ), in exact year ( = ) or after ( > )? ")
        term = input("Which Year? ")
        if checkint(term):
            searchterm = "YearBirth "+ compare + " " + term
        else:
            return
    elif fieldchoice == "d":
        field = "YearDeath"
        compare = input("Do you want results before ( < ), in exact year ( = ) or after ( > )? ")
        term = input("Which Year? ")    
        if checkint(term):
            searchterm = "YearDeath "+ compare + " " + term
        else:
            return
    elif fieldchoice == "r":
        term = input("What Role? ")
        searchterm = "Role LIKE '%"+ term +"%'"
    elif fieldchoice == "t":
        term = input("What Nation? ")
        searchterm = "Nation LIKE '%" + term + "%'"
    else:
        print("Sorry, option not recognised. ")
        return()
    # print (searchterm)
    curs.execute("SELECT * FROM historicalfigures WHERE " + searchterm +";")
    searchdump = curs.fetchall()
    displaytable(searchdump)
    
def enternew():
    print("Entering new record")
    newname = input("Please enter new name: ")
    newbirth = input("Please enter new year of birth: ")
    if not checkint(newbirth):
        print("Not an integer.")
        return
    newdeath = input("Please enter new year of death: ")
    if not checkint(newdeath):
        print("Not an integer.")
        return
    newrole = input("Please enter new role: ")
    newnation = input("Please enter new nation: ")
    keeper = input("Do you want to keep this new record (Y to keep, N to discard): ")
    keeper = keeper.lower()
    if keeper == "y":
        curs.execute("INSERT INTO historicalfigures (Name, YearBirth, YearDeath, Role, Nation) VALUES ('" + newname + "', "+ newbirth +", "+ newdeath +", '" + newrole + "', '" +newnation +"');")
        curs.execute("COMMIT;")
    

def deleterecord():
    curs.execute("SELECT Name FROM historicalfigures ORDER BY Name;")
    namedump = curs.fetchall()
    print("Current list of names is:")
    linecount = 0
    for line in namedump:
        print(linecount + " - " + line[0])
        linecount += 1
    whichline = input("Enter number of row to be deleted: ")
    whichline = int(whichline)
    delname = namedump[whichline][0]
    confirm = input("Please confirm (Y) or cancel (N) deleting record for " + delname +": ")
    confirm = confirm.lower()
    if confirm == 'y':
        curs.execute("DELETE FROM historicalfigures WHERE Name ='" + delname + "';")
        curs.execute("COMMIT;")
    

def updaterecord():
    curs.execute("SELECT Name FROM historicalfigures ORDER BY Name;")
    namedump = curs.fetchall()
    print("Current list of names is:")
    linecount = 0
    for line in namedump:
        print(str(linecount) + " - " + line[0])
        linecount += 1
    whichline = input("Enter number of row to be changed: ")
    whichline = int(whichline)
    updatename = namedump[whichline][0]
    curs.execute("SELECT * FROM historicalfigures WHERE Name = '"+ updatename+"';")  
    changedump = curs.fetchall()
    coltuple = ('Name', 'YearBirth', 'YearDeath', 'Role', 'Nation')
    colcount = 0
    for col in changedump[0]:
        print (str(colcount), coltuple[colcount], changedump[0][colcount])
        colcount += 1
    colchoice = input("Please enter number of column to change: ")
    colchoice = int(colchoice)
    newvalue = input("Please enter new value for this column: ")
    if colchoice == 0 or colchoice == 3 or colchoice == 4:
        newvalue = "'"+ newvalue +"'"
    elif colchoice == 1 or colchoice == 2:
        if not checkint(newvalue):
            return
    print("About to change "+ coltuple[colchoice] +" to " + newvalue + " for "+ updatename)
    confirmchange = input("Confirm(Y) or discard (N): ")
    confirmchange = confirmchange.lower()
    if confirmchange == 'y':
        curs.execute("UPDATE historicalfigures SET "+ coltuple[colchoice] +" = "+ newvalue +" WHERE Name = '" + updatename + "';")
        curs.execute("COMMIT;")

def checkint(num):
    try:
        int(num)
        check = True
    except:
        print("Not a valid number!")
        check = False
    return check
    

#Main program
print ("Historical Figures Table")
print ("========================")
cont = True
while cont == True:
    print ("Main menu - Please select an option")
    print ("View whole table ---V")
    print ("Search table -------S")
    print ("Enter new record ---E")
    print ("Delete record ------D")
    print ("Update record ------U")
    print ("Quit Program -------Q")
    optchoice = input("? ")
    optchoice = optchoice.lower()
    if optchoice == 'v':
        viewtable()
    elif optchoice == 's':
        searchtable()
    elif optchoice == 'e':
        enternew()
    elif optchoice == 'd':
        deleterecord()
    elif optchoice == 'u':
        updaterecord()
    elif optchoice == 'q':
        conn.close()
        cont = False
    else:
        print("Sorry, not recognised")

So this version addresses points 1, 3, 4 and partially 6. Points 2 and 5 require work on the SQLite database, to be done later.
To validate that integers are entered when required, I've added a new function - checkint() which uses try...except... structure. I admit it's not perfect - rather than asking the user to reenter the number, it simply returns to the main menu. This could be improved upon.
The function viewtable() now has an additional step where the user selects what column to sort by. Rather than going through a set of if...elif...else... statements (which I have used in other parts of the program), I've used a dictionary to link the user's choice to the right column.
Finally both viewtable() and searchtable() call the function displaytable() to print it on screen. Unlike most functions in this program, displaytable() takes the output from curs.fetchall() (i.e. a list of tuples) as an argument.

Thursday, 14 September 2017

Errors and Debugging

There may be flawless programmers out there, who type exactly what the computer understands and get exactly the results they require, first time round, no problems or mistakes.
I am not one of those. I get things wrong, sometimes with worrying frequency. My mistakes fall into two broad categories; syntax errors where the computer and Python interpreter don't understand or like what I've typed in and refuse to run the program, and logical errors where the Python interpreter can read and follow my instructions but produce results that I don't want or expect.

Syntax errors

These are generally simple omissions or misspelling. Typical syntax errors that I make include: leaving the colon off the end of a for loop line, misspelling a variable name so the interpreter thinks a new variable is being introduced, starting a statement with an opening parenthesis '(' and then leaving the closing parenthesis ')' off.  Almost unique to Python are errors associated with indentation - most languages don't consider indentation important (they typically use curly braces { } instead), but for Python interpreter, indentation shows how parts of the program are nested in each other. Other error messages will emerge if:
  • you try to import a module Python does not have installed
  • you try to use a function Python does not recognise
  • you get confused between = for assignment and == for comparison (a regular one for me!)
  • type errors are when the interpreter is expecting a number and it gets a string, or it gets a string but was expecting a number. Data types are a whole subject unto themselves, but it is fair to say Python can be quite particular about what data types are used where. 
Syntax errors are fairly easy to detect - the computer will tell you in no uncertain terms that there is something wrong with this program. These error messages can be simple and straightforward, or occasionally indirect and cryptic. The good news is that with practice you get to know what each error message means without needing to study and analyse it too much. We have encountered one of these error messages when trying to convert a string with a decimal point into an integer:
>>> foo = "34.325"
>>> int(foo)
Traceback (most recent call last):
  File "<pyshell#15>", line 1, in <module>
    int(foo)
ValueError: invalid literal for int() with base 10: '34.325'
A lot of syntax errors are picked up by Python when you first tell it to run (F5 in IDLE) but sometimes they crop up while it is running - run-time errors.

Logical Errors

These are not picked up by the interpreter - as far as Python is concerned, the program can run fine. But what you get is not what you want. In the previous post I had the script:
#!/usr/bin/python3
print ("Savings Calculator")
deposit = 100.00
interest = 0.030
year = 2017
savingyears = 20
print(year, deposit)
for i in range(savingyears):
    deposit = deposit + (deposit * interest)
    year += 1
    print (year, deposit)
That was the finished script, but the first version had an error. Instead of
    deposit = deposit + (deposit * interest)
I had
   deposit = deposit * interest
This gave the unintended results of
>>>
===== RESTART: C:/Users/pc/Documents/Programming/test02_errorexample.py =====
Savings Calculator
2017 100.0
2018 3.0
2019 0.09
2020 0.0026999999999999997
2021 8.099999999999999e-05
2022 2.4299999999999996e-06
2023 7.289999999999998e-08
2024 2.1869999999999996e-09
2025 6.560999999999998e-11
2026 1.968299999999999e-12
2027 5.904899999999998e-14
2028 1.7714699999999992e-15
2029 5.3144099999999976e-17
2030 1.5943229999999992e-18
2031 4.7829689999999975e-20
2032 1.4348906999999993e-21
2033 4.3046720999999973e-23
2034 1.2914016299999992e-24
2035 3.8742048899999977e-26
2036 1.1622614669999993e-27
2037 3.486784400999998e-29
>>>
 I really hope my bank account does not have that sort of interest rate. For those not mathematically minded, the part where it says e-y it is shorthand for the first number divided by 10 y times. Again, the computer does not recognise anything wrong - it has no problem doing what it is told. The problem is I have told it to do the wrong thing.

Debugging

There is no magic bullet here that can instantly remove bugs before you know what's going on. Instead I have a basic process for debugging
  • I look at the output, including any error messages and any line numbers mentioned
  • I look at my script and see what might have gone wrong
  • I make whatever corrections I think are needed
  • I run the program again
  • If the program runs as intended, then it has been successfully debugged. 
  • If not, and it is the same or similar problem that shows up, I go back to the start of this list
  • Sometimes I solve one bug, run the program and find that there are several different bugs - I now have to deal with the next one in line. 
I have occasionally been hindered by a rather elusive bug. Here print statements at particular points in the program can show onscreen what certain variables are at these points. If the printed results are what I want and expect then I can assume that the program is functioning as intended up to that point, and the bug is further on in the code.