5. Basic Input and Output#

Programs that do not alter their processing based upon different inputs are boring - they do the same thing repeatedly. A program’s usefulness increases exponentially with the information the code can receive and process.

Except for the code presented in the first notebook, we have not received any input. Therefore, if we needed to change the program’s behavior, we had to change the program.

This Python notebook presents the primary mechanism to receive input from the console (aka the user’s keyboard).

5.1. Input#

As programs often require users to provide data, we can receive input from the user using Python’s built-in function input().

Python documentation for input

input() takes an optional argument, which is the prompt to the user. The Python interpreter writes this prompt to the console (screen, terminal session, shell window) without a trailing newline. input() pauses the program execution and waits for the user to enter a value and then type the enter key. The function strips the newline character and returns the value as a string type.

1print("Hello, my name is Hal 9000.")
2name = input("What is your name?")
3print("It is nice to meet you,", name)
Hello, my name is Hal 9000.
What is your name?Steve
It is nice to meet you, Steve
1print("Enter two numbers to add (try 1968 and 2001):")
2num_1 = input("first number: ")
3num_2 = input("second number: ")
4print(num_1 + num_2)
Enter two numbers to add (try 1968 and 2001):
first number: 1968
second number: 2001
19682001

Whoa, that was not the expected result. I thought adding 1968 and 2001 would be 3969.

Remember, input() always returns a string. Therefore, to use the input value as a number value, we need to convert it using int() or float(), as presented in the previous notebook.

1print("Enter two numbers to add:")
2string_1 = input("first number: ")
3num_1 = int(string_1)
4num_2 = int(input("second number: "))
5print(num_1 + num_2)
Enter two numbers to add:
first number: 1968
second number: 2001
3969

Much better.

Note that for the first input value, we assigned the string value to a variable and then converted that variable to another variable containing an integer value. Then for the second input value, we immediately converted the return value from the input() function to an integer value without using an intermediary variable. The function calls were nested. As the interpreter evaluates nested functions, the interpreter evaluates the innermost function first.

Beginners might find the first pattern more comforting as one can see more explicitly what occurs (and it is also a little easier when debugging programs to see values. We will discuss debugging when we discuss IDEs (specifically Visual Studio Code). However, advanced programmers usually prefer the second pattern as the code is more concise.

What occurs if we ask the user to enter a number, but the user does not enter a valid integer?

1year = int(input("Enter the year your were born: "))   # Enter "two thousand", raises "ValueError"
Enter the year your were born: two thousand
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [5], line 1
----> 1 year = int(input("Enter the year your were born: "))

ValueError: invalid literal for int() with base 10: 'two thousand'

As seen in the previous notebook, we receive a “ValueError”. We will learn to create more robust error handling mechanisms in later modules.

5.2. Output#

The primary method we use to print output to the console for a user to see is the print() function.

1help(print)
Help on built-in function print in module builtins:

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
    
    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file:  a file-like object (stream); defaults to the current sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.

While all our examples with print() have used a single argument, we can pass multiple variables and literal values to the function. Additionally, we can override defaults for the separator value between values, the string appended after the last value, and where the output goes.

1print(1,2,3,4,5,sep=':')
1:2:3:4:5
1print("Hello",end="")
2print("World")
HelloWorld

5.3. Readability#

Programmers use many different conventions to improve code readability. For example, you have already seen how descriptive variable names can clarify their purpose.

5.3.1. Comments#

As with most other programming languages, Python allows code comments.

Comments start with a # and go to the end of the line. The Python interpreter ignores a comment’s content.

Use comments to

  • explain code

  • make code more readable

  • clarify constants and variables

  • prevent executing lines (place a # before any code in the line) while manually testing code

  • as a place marker for a “To Do” item

1seconds_per_day = 86400   #  60 seconds/minute * 60 minutes/hour * 24 hours/day
2
3# Demonstrates that the type returned by input() is a string
4# Try using 1968 and 2001.  While we might expect 3969 to be return 19682001 will be printed
5print("Enter two numbers to add:")
6num_1 = input("first number: ")
7num_2 = input("second number: ")
8print(num_1 + num_2)
9print(type(num_1))
Enter two numbers to add:
first number: 1968
second number: 2001
19682001
<class 'str'>

Best Practices for Writing Code Comments

5.3.2. Whitespace#

To separate logical blocks of code, you can use blank lines, which the Python interpreter ignores. Additionally, you can use space characters before and after operators, keywords, and variables to make code easier to read.

5.3.3. Splitting Lengthy Statements#

By convention, the maximum suggested length for python lines of code is 79 characters. With this length, programmers can place two code editing windows side by side. This length also supports code review tools that present two versions in adjacent columns/windows. Some teams may choose to adopt a longer standard.

To split a longer statement over two or more lines, place a \ at the end of the line to continue the statement on the next line.

Programmers can also place arguments into different lines in function and method calls after a comma.

1sum_of_numbers = 1 + \
2                 2 + \
3                 3 + \
4                 4 + \
5                 5
6print("Here is the sum of numbers -",
7      sum_of_numbers)
Here is the sum of numbers - 15

5.4. Case Study: Systematic Investment Plan Calculator#

Earlier, we presented a design process to produce these parts of a design:

  • Inputs to the routine

  • Outputs from the routine

  • Pseudocode (steps in the routine).

The design process followed Seven Steps:[1]

We now follow that process for creating a systematic investment plan (SIP) calculator. A SIP is an investment product offered by many mutual fund companies. Investors periodically put a fixed amount of money into the SIP (e.g., monthly) to help promote financial discipline and provide dollar-cost averaging.

\( FV = P \times \dfrac{(1+i)^n - 1}{i} \times (1 +i) \)

  • \(FV\) is the future value

  • \(P\) is the amount invested at the start of every payment interval

  • \(i\) is the periodic interest rate; for a monthly payment, take the (Annual Rate)/12

  • \(n\) is the number of payments

So our goal is to develop a calculator in which the user enters an investment amount, the number of payments, and a periodic interest rate. The calculator then produces the future value amount and displays that amount.

5.4.1. Step 1: Work an Instance Manually#

We open up the calculator app on our smartphone and calculate the future value of \(\$100\) payments made for 360 months, assuming an annual return of \(7%\) (historical average of the DJIA). Periodic rate = .07/12 = 0.00583.

\( 100 * ((1+0.00583)^{360} -1 )/.00583 * (1+0.00583) = 122,708.50\)

5.4.2. Step 2: Document the steps#

  1. Use $100 payment amount

  2. Use 360 for the number of payments

  3. Use 0.07 as the annual interest rate

  4. Convert 0.07 to the monthly periodic rate (0.07/12 ~= 0.00583)

  5. Compute the formula with a calculator.

Remember, our goal in these first two steps is to work a specific instance by hand. While it’s tempting to jump to generalizing the steps, stepping through these first two steps allows us to find subtleties and other issues we may have overlooked otherwise.

5.4.3. Step 3: Generalize our steps#

We need to add the inputs and outputs - the steps are pretty much the same, except we need to make those initial steps generic by getting the values rather than using fixed values.

  • Inputs: payment amount, number of payments, and annual interest rate.

  • Outputs future value.

Algorithm:

  1. Get the payment amount

  2. Get the number of payments

  3. Get the annual interest rate

  4. Convert the annual interest rate into our periodic investment rate (monthly)

  5. Calculate the future value

  6. Display the result to the user

5.4.4. Step 4: Manually test our generic process#

As you start this step, look to develop a set of cases that will thoroughly test your process. Start with the expected inputs. For this problem, payments could be a reasonable amount to save every month. Yes, this depends on your income. Assume $100. For the number of payments, assume we are saving for retirement. Assume 30 years. Investment sales pitches use more extended time frames as they demonstrate the time value of money and the power of compound interest. For the interest rate, we can use 7% as that value is the historical average of the DJIA. Compute the future value.

Now create test cases (i.e., different input numbers) that could discover potential problems. One approach could be to look at the boundary values. Often for numbers, this will occur around 0. So use -1,0,1 for the interest rate possibilities. At the same, you will also need to consider if two test cases are equivalent. For example, can we expect an execution difference between 1% and 7%? Probably not.

Looking at the boundaries is more formally called “boundary value analysis”. People widely recognize that values at the extreme ends of the input possibilities tend to cause more errors. Zero also tends to be problematic.

Examples of issues with zeros in real systems:

Considering two input values to be “equivalent” is called “equivalence partitioning” and is used to reduce the total number of test cases to a more manageable number. As interest rates are floating-point numbers, does it make sense to test all possible values? .01, .02, .03, .031, .032, .03201, .032001. No value-add exists to enumerate these values exhaustively. Perform a couple and do something with more potential to find problems.

What happens if we use -0.01 as the interest rate? Calculation returns \(\$\)31,087.00 - seems plausible.

What happens with 0? Oops, we just divided by zero. Good thing we are not on that navy ship suffering the blue screen of death.

5.4.5. Algorithm to Code#

We focus on converting our algorithm to code in the last three steps. As the Seven Steps diagram indicates, these steps are an iterative process. Further, it is not necessary to complete all of a particular step before moving on to the next step. For example, you may want to convert parts of the algorithm first to code, validate that code, make any necessary changes(debug), and repeat this process until the code reflects the entire algorithm.

5.4.6. Step 5: Code#

In this step, we convert our pseudocode into Python statements. Ideally, this step should be straightforward, as each documented step should convert into an equivalent line of code. For example, “Get the payment amount” implies an input() function call with a prompt alerting the user to enter the payment amount.

5.4.7. Step 6: Test#

In this step, we execute our source code to validate our program works correctly. Often we can repeat the same test cases we used in Step 4. More advanced programmers will first write automated test cases to validate their code.

5.4.8. Step 7: Debug#

Debugging is the process of fixing issues found in your code. These issues belong to different error categories: syntactic, runtime, and semantic.

Syntactic errors occur when we do not follow the formal grammar defined by the programming language. For example, we forget to include parenthesis around calls to a function.

1print "This causes a syntax" error..
  Cell In [11], line 1
    print "This causes a syntax" error..
    ^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?

These issues are generally the easiest to solve. The interpreter immediately stops the program execution and reports the error. Fix the code block and re-execute.

1print("Now this code runs correctly.")
Now this code runs correctly.

Runtime errors occur when the Python interpreter executes legally syntactic code, but there is some issue with the code, and the interpreter cannot continue normal execution. Examples of runtime errors include not defining variables before use and division by zero. The interpreter indicates these errors with the presence of a “Traceback” - statement sequences and function calls that led to the error. These errors may or may not be easy to fix. Sometimes the variable not being defined is just a typo. Other times, we need to see how the particular condition that led to that error arose.

Semantic errors (logic errors) occur when the program executes without issues but produces the wrong result. These issues usually arise due to a logical mistake. Completely understanding all of the different possible inputs a program may process and how our code reacts to those inputs can be challenging. Our minds can only track so many possibilities simultaneously. Other times we may be using code or services that others developed; that code may react in unexpected ways.

As you find and fix issues, several strategies exist:

Replicate: In more complex situations, replicating a particular issue can be a challenge in and of itself. However, replication is an essential task. Once you replicate an issue, you can understand the inputs and other ancillary factors that led to the problem.

Read: Read your source code. Walk through it mentally. Does the code execute the way you intended?

Run: Execute your code. Step through your code with a debugger or add print statements to the code to print the program’s current state. Experiment with running slightly different code versions to see what occurs. This strategy is not to randomly try random things (e.g., throwing something at a wall until it sticks) but rather to systematically investigate an issue.

Reflect: Reflect on the issue. What is the interpreter telling you? How could the code and inputs cause the issue? If you recently made a change, did that change have any impact? With this strategy, you should develop “educated guesses” about what may be the cause. Then look to see how you can test to validate or eliminate those possibilities.

“Ask a Friend”: Try explaining your code and issues to a fellow student or coworker. Sometimes, this process lets you find the issue on your own in a moment of “oh duh, that is what happened”. If not, the other person may have some good ideas to investigate.

Pause: Sometimes, the best approach is to step back. Grab a beverage and take a break.

Research: Do you have enough problem domain knowledge to understand the problem and implement a solution?

Step Back: If you have a prior code version, revert to that. Or maybe delete recently added code. Get back to a working program. Then re-think the problem. Take some notes. Then add code back into, testing as you proceed.

Unfortunately, no magical solution exists to make someone an expert debugger - it takes experience. One of the choices made in these notebooks has been demonstrating software issues(defects) that can arise. By seeing these issues and correcting these issues, you are more prepared to handle issues in your code.

The term debugging dates back to 1878 with a letter Thomas Edison wrote to Theodore Puskas[2]:

Bugs’ – as such little faults and difficulties are called – show themselves and months of intense watching, study and labor are requisite before commercial success or failure is certainly reached.

In 1947, operators found a moth trapped in a relay in the Mark II Computer at Harvard University:

Caption: The First “Computer Bug” Source

5.4.9. “Executing” Steps 5, 6, and 7#

In the following code blocks, we follow the process of converting our pseudocode into code, testing, and making fixes. These three steps are inherently iterative.

As you begin to write code, you should not attempt to write the entire process at once. Instead, write a few steps, test those, and make any changes to get that smaller piece of code working. Then continue to write more of the steps for your process.

1payment = input("Enter the periodic payment: ")
2annual_rate = input("Enter the annual rate: ")
3periodic_rate = annual_rate / 12
4print(periodic_rate)
Enter the periodic payment: 500
Enter the annual rate: 0.05
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [13], line 3
      1 payment = input("Enter the periodic payment: ")
      2 annual_rate = input("Enter the annual rate: ")
----> 3 periodic_rate = annual_rate / 12
      4 print(periodic_rate)

TypeError: unsupported operand type(s) for /: 'str' and 'int'

Oops, we forgot to convert our input values (which are strings) to floats.

1payment = float(input("Enter the periodic payment: "))
2annual_rate = float(input("Enter the annual rate: "))
3periodic_rate = annual_rate / 12
4print(periodic_rate)
Enter the periodic payment: 500
Enter the annual rate: 0.05
0.004166666666666667

Good. That works now.

1payment = float(input("Enter the periodic payment: "))
2num_payments = int(inpt("Enter the number of payments: "))
3periodic_rate = float(input("Enter the annual rate: "))/12
4
5future_value = payment *  ((1 + periodic_rate)^num_payments - 1)/periodic_rate * (1 + periodic_rate)
6print(future_value)
Enter the periodic payment: 500
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [15], line 2
      1 payment = float(input("Enter the periodic payment: "))
----> 2 num_payments = int(inpt("Enter the number of payments: "))
      3 periodic_rate = float(input("Enter the annual rate: "))/12
      5 future_value = payment *  ((1 + periodic_rate)^num_payments - 1)/periodic_rate * (1 + periodic_rate)

NameError: name 'inpt' is not defined

sigh, misspelled input.

1payment = float(input("Enter the periodic payment: "))
2num_payments = int(input("Enter the number of payments: "))
3periodic_rate = float(input("Enter the annual rate: "))/12
4
5future_value = payment *  ((1 + periodic_rate)^num_payments - 1)/periodic_rate * (1 + periodic_rate)
6print(future_value)
Enter the periodic payment: 500
Enter the number of payments: 60
Enter the annual rate: 0.05
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [17], line 5
      2 num_payments = int(input("Enter the number of payments: "))
      3 periodic_rate = float(input("Enter the annual rate: "))/12
----> 5 future_value = payment *  ((1 + periodic_rate)^num_payments - 1)/periodic_rate * (1 + periodic_rate)
      6 print(future_value)

TypeError: unsupported operand type(s) for ^: 'float' and 'int'

^ is not the correct symbol for the exponent operator. Geez, Visual Basic uses that. looks through past notebooks. Oh yeah, ** is the exponent operator in Python.

1payment = float(input("Enter the periodic payment: "))
2num_payments = int(input("Enter the number of payments: "))
3periodic_rate = float(input("Enter the annual rate: "))/12
4
5future_value = payment *  ((1+periodic_rate)**num_payments -1)/periodic_rate * (1+periodic_rate)
6print(future_value)
Enter the periodic payment: 500
Enter the number of payments: 60
Enter the annual rate: 0.05
34144.72075967318

Looks good, but can we format the output better? (A later notebook presents string formatting.)

1payment = float(input("Enter the periodic payment: "))
2num_payments = int(input("Enter the number of payments: "))
3periodic_rate = float(input("Enter the annual rate: "))/12
4
5future_value = payment *  ((1+periodic_rate)**num_payments -1)/periodic_rate * (1+periodic_rate)
6print("Future value: ${:,.2f}".format(future_value))
Enter the periodic payment: 500
Enter the number of payments: 60
Enter the annual rate: 0.05
Future value: $34,144.72

Now, try some more test cases. What happens with a large interest rate? We want to be rich after all.

1payment = float(input("Enter the periodic payment: "))
2num_payments = int(input("Enter the number of payments: "))
3periodic_rate = float(input("Enter the annual rate: "))/12
4
5future_value = payment *  ((1+periodic_rate)**num_payments -1)/periodic_rate * (1+periodic_rate)
6print("Future value: ${:,.2f}".format(future_value))
Enter the periodic payment: 1000
Enter the number of payments: 1000
Enter the annual rate: 1000
---------------------------------------------------------------------------
OverflowError                             Traceback (most recent call last)
Cell In [24], line 5
      2 num_payments = int(input("Enter the number of payments: "))
      3 periodic_rate = float(input("Enter the annual rate: "))/12
----> 5 future_value = payment *  ((1+periodic_rate)**num_payments -1)/periodic_rate * (1+periodic_rate)
      6 print("Future value: ${:,.2f}".format(future_value))

OverflowError: (34, 'Result too large')

We received an overflow error as our number was too large for Python to represent as a float number. You should try 0 for the interest rate, which causes a division by zero error. Do not worry about fixing these test cases now. In later notebooks, we examine how to make our code more robust through input validation and exception handling. Conveniently in the next notebook, we discuss making decisions in our code with the if statement. The if statement would allow us to test if the interest rate equals zero and then compute the future value as the number of payments * payment amount.

Reflecting on the above example, we assumed the periods were months. How could you generalize that? What changes would you make?

Step through the code at PytonTutor

5.5. Suggested LLM Prompts#

  • List 5 strategies to use when debugging a Python program. How can I use those strategies within Jupyter lab?

  • Why do I need to convert types in Python? Can’t the language infer what I want to do?

  • List 5 strategies for using comments in Python code. Provide examples.

  • Provide 5 anti-patterns for comments in Python with examples. Also ask, what are anti-patterns in software development?

  • Using Python, demonstrate how I can translate pseudocode to code. As an example problem, compute compound interest based on user input.

  • Why does this code not work correctly?

    print("Enter two numbers to add (try 1968 and 2001):")
    num_1 = input("first number: ")
    num_2 = input("second number: ")
    print(num_1 + num_2)
    

5.6. Review Questions#

  1. Why is input and output so fundamental to computer programs?

  2. What is the data type returned by input()? How can we convert that value to other types?

  3. Why is readability important?

  4. How are comments identified in Python?

  5. How many arguments does input() require?

  6. How many arguments can print() have?

answers

5.7. Exercises#

  1. What is wrong with this code? Make the appropriate changes to run the code correctly.

interest_rate = input("Enter an interest rate")
print("On 1000 dollars, you will owe ", 1000 * interest_rate)
  1. Using periodic compounding, compute the future value of an investment. Have the user enter the initial principal, the interest rate, and the number of years. Assume the interest compounds monthly. This exercise extends a previous exercise with user inputs rather than inputs placed directly into the code.

  2. Write a program to determine the monthly payment for a 30-year mortgage for an initial principal amount and interest rate that the user specifies: Formula

\( A = P \times \dfrac{r(1+r)^n}{(1+r)^n - 1} \)

  • \(A\) is the periodic amortization (monthly) payment

  • \(P\) is the principal amount borrowed

  • \(r\) is the rate of interest expressed as a fraction; for a monthly payment, take the (Annual Rate)/12

  • \(n\) is the number of payments; for monthly payments over 30 years, 12 months x 30 years = 360 payments.

5.8. References#

[1] Andrew D. Hilton, Genevieve M. Lipp, and Susan H. Rodger. 2019. Translation from Problem to Code in Seven Steps. In Proceedings of the ACM Conference on Global Computing Education (CompEd ‘19). Association for Computing Machinery, New York, NY, USA, 78–84. https://doi-org.prox.lib.ncsu.edu/10.1145/3300115.3309508
[2] https://www.computerworld.com/article/2515435/moth-in-the-machine–debugging-the-origins-of–bug-.html